diff --git a/src/Models/RefName.cs b/src/Models/RefName.cs new file mode 100644 index 000000000..929a0dcd4 --- /dev/null +++ b/src/Models/RefName.cs @@ -0,0 +1,100 @@ +using System; + +namespace SourceGit.Models +{ + /// + /// Validates branch, tag and remote names using the same rules that git itself enforces, + /// mirroring `git check-ref-format --allow-onelevel`. Rules are taken from the git-check-ref-format documentation. + /// Rule 2 (a refname must contain at least one slash) is intentionally waived, matching `--allow-onelevel`. + /// + public static class RefName + { + public static bool IsValidBranchName(string name) + { + if (string.IsNullOrEmpty(name)) + return false; + + if (name.StartsWith('-')) + return false; + + if (string.Equals(name, "HEAD", StringComparison.Ordinal)) + return false; + + return IsValidRefName(name); + } + + public static bool IsValidTagName(string name) + { + if (string.IsNullOrEmpty(name)) + return false; + + if (name.StartsWith('-')) + return false; + + return IsValidRefName(name); + } + + public static bool IsValidRemoteName(string name) + { + if (string.IsNullOrEmpty(name)) + return false; + + return IsValidRefName(name); + } + + private static bool IsValidRefName(string name) + { + // Rule 9: cannot be the single character '@'. + if (name.Equals("@", StringComparison.Ordinal)) + return false; + + // Rule 6: cannot begin or end with '/', or contain consecutive slashes. + if (name[0] == '/' || name[^1] == '/' || name.Contains("//", StringComparison.Ordinal)) + return false; + + // Rule 7: cannot end with a dot. + if (name[^1] == '.') + return false; + + // Rule 3: cannot contain two consecutive dots. + if (name.Contains("..", StringComparison.Ordinal)) + return false; + + // Rule 8: cannot contain the sequence '@{'. + if (name.Contains("@{", StringComparison.Ordinal)) + return false; + + // Rules 4, 5 & 10: no control chars, DEL, space, or ~ ^ : ? * [ \. + foreach (var ch in name) + { + if (ch is < ' ' or '\x7f') + return false; + + switch (ch) + { + case ' ': + case '~': + case '^': + case ':': + case '?': + case '*': + case '[': + case '\\': + return false; + } + } + + // Rule 1: no slash-separated component may begin with a dot or end with ".lock". + foreach (var component in name.Split('/')) + { + if (component[0] == '.') + return false; + + if (component.EndsWith(".lock", StringComparison.Ordinal)) + return false; + } + + return true; + } + } +} diff --git a/src/ViewModels/AddRemote.cs b/src/ViewModels/AddRemote.cs index f5182ef9f..f503803d0 100644 --- a/src/ViewModels/AddRemote.cs +++ b/src/ViewModels/AddRemote.cs @@ -7,7 +7,6 @@ namespace SourceGit.ViewModels public class AddRemote : Popup { [Required(ErrorMessage = "Remote name is required!!!")] - [RegularExpression(@"^[\w\-\.]+$", ErrorMessage = "Bad remote name format!!!")] [CustomValidation(typeof(AddRemote), nameof(ValidateRemoteName))] public string Name { @@ -59,6 +58,9 @@ public static ValidationResult ValidateRemoteName(string name, ValidationContext { if (ctx.ObjectInstance is AddRemote add) { + if (!Models.RefName.IsValidRemoteName(name)) + return new ValidationResult("Bad remote name format!!!"); + var exists = add._repo.Remotes.Find(x => x.Name == name); if (exists != null) return new ValidationResult("A remote with given name already exists!!!"); diff --git a/src/ViewModels/CheckoutBranchFromStash.cs b/src/ViewModels/CheckoutBranchFromStash.cs index d62f4f2df..a87561805 100644 --- a/src/ViewModels/CheckoutBranchFromStash.cs +++ b/src/ViewModels/CheckoutBranchFromStash.cs @@ -12,7 +12,6 @@ public Models.Stash Target } [Required(ErrorMessage = "Branch name is required!")] - [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(CheckoutBranchFromStash), nameof(ValidateBranchName))] public string BranchName { @@ -30,6 +29,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (ctx.ObjectInstance is CheckoutBranchFromStash caller) { + if (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + foreach (var b in caller._repo.Branches) { if (b.FriendlyName.Equals(name, StringComparison.Ordinal)) diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index f180c94e9..f49c03e65 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -7,7 +7,6 @@ namespace SourceGit.ViewModels public class CreateBranch : Popup { [Required(ErrorMessage = "Branch name is required!")] - [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(CreateBranch), nameof(ValidateBranchName))] public string Name { @@ -115,6 +114,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (ctx.ObjectInstance is CreateBranch creator) { + if (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + if (!creator._allowOverwrite) { foreach (var b in creator._repo.Branches) diff --git a/src/ViewModels/CreateTag.cs b/src/ViewModels/CreateTag.cs index ff729aab9..6c15b8b89 100644 --- a/src/ViewModels/CreateTag.cs +++ b/src/ViewModels/CreateTag.cs @@ -13,7 +13,6 @@ public object BasedOn } [Required(ErrorMessage = "Tag name is required!")] - [RegularExpression(@"^(?!\.)(?!/)(?!.*\.$)(?!.*/$)(?!.*\.\.)[\w\-\+\./]+$", ErrorMessage = "Bad tag name format!")] [CustomValidation(typeof(CreateTag), nameof(ValidateTagName))] public string TagName { @@ -74,6 +73,9 @@ public static ValidationResult ValidateTagName(string name, ValidationContext ct { if (ctx.ObjectInstance is CreateTag creator) { + if (!Models.RefName.IsValidTagName(name)) + return new ValidationResult("Bad tag name format!"); + var found = creator._repo.Tags.Find(x => x.Name == name); if (found != null) return new ValidationResult("A tag with same name already exists!"); diff --git a/src/ViewModels/EditRemote.cs b/src/ViewModels/EditRemote.cs index 84b950e2a..a2431e979 100644 --- a/src/ViewModels/EditRemote.cs +++ b/src/ViewModels/EditRemote.cs @@ -7,7 +7,6 @@ namespace SourceGit.ViewModels public class EditRemote : Popup { [Required(ErrorMessage = "Remote name is required!!!")] - [RegularExpression(@"^[\w\-\.]+$", ErrorMessage = "Bad remote name format!!!")] [CustomValidation(typeof(EditRemote), nameof(ValidateRemoteName))] public string Name { @@ -60,6 +59,9 @@ public static ValidationResult ValidateRemoteName(string name, ValidationContext { if (ctx.ObjectInstance is EditRemote edit) { + if (!Models.RefName.IsValidRemoteName(name)) + return new ValidationResult("Bad remote name format!!!"); + foreach (var remote in edit._repo.Remotes) { if (remote != edit._remote && name == remote.Name) diff --git a/src/ViewModels/GitFlowStart.cs b/src/ViewModels/GitFlowStart.cs index 2fe9c0784..68f9f88e4 100644 --- a/src/ViewModels/GitFlowStart.cs +++ b/src/ViewModels/GitFlowStart.cs @@ -32,7 +32,6 @@ public string Prefix } [Required(ErrorMessage = "Name is required!!!")] - [RegularExpression(@"^[\w\-/\.#]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(GitFlowStart), nameof(ValidateBranchName))] public string Name { @@ -67,6 +66,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (ctx.ObjectInstance is GitFlowStart starter) { + if (!Models.RefName.IsValidBranchName($"{starter.Prefix}{name}")) + return new ValidationResult("Bad branch name format!"); + var check = $"{starter.Prefix}{name}"; foreach (var b in starter._repo.Branches) { diff --git a/src/ViewModels/InitGitFlow.cs b/src/ViewModels/InitGitFlow.cs index f00ad01dd..798120b55 100644 --- a/src/ViewModels/InitGitFlow.cs +++ b/src/ViewModels/InitGitFlow.cs @@ -12,7 +12,6 @@ public partial class InitGitFlow : Popup private static partial Regex REG_TAG_PREFIX(); [Required(ErrorMessage = "Production branch name is required!!!")] - [RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))] public string Production { @@ -21,7 +20,6 @@ public string Production } [Required(ErrorMessage = "Development branch name is required!!!")] - [RegularExpression(@"^[\w\-/\.]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(InitGitFlow), nameof(ValidateBaseBranch))] public string Develop { @@ -81,10 +79,13 @@ public InitGitFlow(Repository repo) _production = "main"; } - public static ValidationResult ValidateBaseBranch(string _, ValidationContext ctx) + public static ValidationResult ValidateBaseBranch(string name, ValidationContext ctx) { if (ctx.ObjectInstance is InitGitFlow initializer) { + if (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + if (initializer._production == initializer._develop) return new ValidationResult("Develop branch has the same name with production branch!"); } diff --git a/src/ViewModels/RenameBranch.cs b/src/ViewModels/RenameBranch.cs index dbca651e7..72d1a4115 100644 --- a/src/ViewModels/RenameBranch.cs +++ b/src/ViewModels/RenameBranch.cs @@ -12,7 +12,6 @@ public Models.Branch Target } [Required(ErrorMessage = "Branch name is required!!!")] - [RegularExpression(@"^[\w\-/\.#\+]+$", ErrorMessage = "Bad branch name format!")] [CustomValidation(typeof(RenameBranch), nameof(ValidateBranchName))] public string Name { @@ -31,6 +30,9 @@ public static ValidationResult ValidateBranchName(string name, ValidationContext { if (ctx.ObjectInstance is RenameBranch rename) { + if (!Models.RefName.IsValidBranchName(name)) + return new ValidationResult("Bad branch name format!"); + foreach (var b in rename._repo.Branches) { if (b.IsLocal && b != rename.Target && b.Name.Equals(name, StringComparison.Ordinal))