Skip to content

Commit 020c076

Browse files
Copilotmitchdenny
andcommitted
Revert to validation logic and simplify to only reject path separators
Co-authored-by: mitchdenny <[email protected]>
1 parent 1fd9a51 commit 020c076

File tree

3 files changed

+198
-1
lines changed

3 files changed

+198
-1
lines changed

src/Aspire.Cli/Commands/NewCommand.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.CommandLine;
5+
using System.Text.RegularExpressions;
56
using Aspire.Cli.Certificates;
67
using Aspire.Cli.Configuration;
78
using Aspire.Cli.DotNet;
@@ -12,6 +13,7 @@
1213
using Aspire.Cli.Templating;
1314
using Aspire.Cli.Utils;
1415
using Semver;
16+
using Spectre.Console;
1517
using NuGetPackage = Aspire.Shared.NuGetPackageCli;
1618

1719
namespace Aspire.Cli.Commands;
@@ -201,6 +203,9 @@ public virtual async Task<string> PromptForProjectNameAsync(string defaultName,
201203
return await interactionService.PromptForStringAsync(
202204
NewCommandStrings.EnterTheProjectName,
203205
defaultValue: defaultName,
206+
validator: name => ProjectNameValidator.IsProjectNameValid(name)
207+
? ValidationResult.Success()
208+
: ValidationResult.Error(NewCommandStrings.InvalidProjectName),
204209
cancellationToken: cancellationToken);
205210
}
206211

@@ -214,3 +219,24 @@ public virtual async Task<ITemplate> PromptForTemplateAsync(ITemplate[] validTem
214219
);
215220
}
216221
}
222+
223+
internal static partial class ProjectNameValidator
224+
{
225+
// Regex for project name validation:
226+
// - Can be any characters except path separators (/ and \)
227+
// - Length: 1-254 characters
228+
// - Must not be empty or whitespace only
229+
[GeneratedRegex(@"^[^/\\]{1,254}$", RegexOptions.Compiled)]
230+
internal static partial Regex GetProjectNameRegex();
231+
232+
public static bool IsProjectNameValid(string projectName)
233+
{
234+
if (string.IsNullOrWhiteSpace(projectName))
235+
{
236+
return false;
237+
}
238+
239+
var regex = GetProjectNameRegex();
240+
return regex.IsMatch(projectName);
241+
}
242+
}

src/Aspire.Cli/Templating/DotNetTemplateFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ private async Task<TemplateResult> ApplyTemplateAsync(CallbackTemplate template,
313313

314314
private async Task<string> GetProjectNameAsync(ParseResult parseResult, CancellationToken cancellationToken)
315315
{
316-
if (parseResult.GetValue<string>("--name") is not { } name)
316+
if (parseResult.GetValue<string>("--name") is not { } name || !ProjectNameValidator.IsProjectNameValid(name))
317317
{
318318
var defaultName = new DirectoryInfo(Environment.CurrentDirectory).Name;
319319
name = await prompter.PromptForProjectNameAsync(defaultName, cancellationToken);
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Cli.Commands;
5+
6+
namespace Aspire.Cli.Tests.Commands;
7+
8+
public class ProjectNameValidatorTests
9+
{
10+
[Theory]
11+
[InlineData("项目1", true)] // Chinese
12+
[InlineData("Проект1", true)] // Cyrillic
13+
[InlineData("プロジェクト1", true)] // Japanese
14+
[InlineData("مشروع1", true)] // Arabic
15+
[InlineData("Project_1", true)] // Latin with underscore
16+
[InlineData("Project-1", true)] // Latin with dash
17+
[InlineData("Project.1", true)] // Latin with dot
18+
[InlineData("MyApp", true)] // Simple ASCII
19+
[InlineData("A", true)] // Single character
20+
[InlineData("1", true)] // Single number
21+
[InlineData("プ", true)] // Single Unicode character
22+
[InlineData("Test123", true)] // Mixed letters and numbers
23+
[InlineData("My_Cool-Project.v2", true)] // Complex valid name
24+
[InlineData("Project:1", true)] // Colon (now allowed)
25+
[InlineData("Project*1", true)] // Asterisk (now allowed)
26+
[InlineData("Project?1", true)] // Question mark (now allowed)
27+
[InlineData("Project\"1", true)] // Quote (now allowed)
28+
[InlineData("Project<1", true)] // Less than (now allowed)
29+
[InlineData("Project>1", true)] // Greater than (now allowed)
30+
[InlineData("Project|1", true)] // Pipe (now allowed)
31+
[InlineData("Project ", true)] // Ends with space (now allowed)
32+
[InlineData(" Project", true)] // Starts with space (now allowed)
33+
[InlineData("Pro ject", true)] // Space in middle (now allowed)
34+
[InlineData("-Project", true)] // Starts with dash (now allowed)
35+
[InlineData("Project-", true)] // Ends with dash (now allowed)
36+
[InlineData(".Project", true)] // Starts with dot (now allowed)
37+
[InlineData("Project.", true)] // Ends with dot (now allowed)
38+
[InlineData("_Project", true)] // Starts with underscore (now allowed)
39+
[InlineData("Project_", true)] // Ends with underscore (now allowed)
40+
public void IsProjectNameValid_ValidNames_ReturnsTrue(string projectName, bool expected)
41+
{
42+
// Act
43+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
44+
45+
// Assert
46+
Assert.Equal(expected, result);
47+
}
48+
49+
[Theory]
50+
[InlineData("Project/1", false)] // Forward slash (path separator)
51+
[InlineData("Project\\1", false)] // Backslash (path separator)
52+
[InlineData("", false)] // Empty string
53+
[InlineData(" ", false)] // Space only
54+
[InlineData(" ", false)] // Multiple spaces only
55+
[InlineData("\t", false)] // Tab only
56+
[InlineData("\n", false)] // Newline only
57+
public void IsProjectNameValid_InvalidNames_ReturnsFalse(string projectName, bool expected)
58+
{
59+
// Act
60+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
61+
62+
// Assert
63+
Assert.Equal(expected, result);
64+
}
65+
66+
[Fact]
67+
public void IsProjectNameValid_MaxLength254_ReturnsTrue()
68+
{
69+
// Arrange
70+
var projectName = new string('A', 254);
71+
72+
// Act
73+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
74+
75+
// Assert
76+
Assert.True(result);
77+
}
78+
79+
[Fact]
80+
public void IsProjectNameValid_Length255_ReturnsFalse()
81+
{
82+
// Arrange
83+
var projectName = new string('A', 255);
84+
85+
// Act
86+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
87+
88+
// Assert
89+
Assert.False(result);
90+
}
91+
92+
[Theory]
93+
[InlineData("项目测试名称很长的中文项目名称")] // Long Chinese name
94+
[InlineData("очень_длинное_русское_имя_проекта")] // Long Russian name
95+
[InlineData("とても長い日本語のプロジェクト名")] // Long Japanese name
96+
[InlineData("اسم_مشروع_طويل_جدا_بالعربية")] // Long Arabic name
97+
public void IsProjectNameValid_LongUnicodeNames_ReturnsTrue(string projectName)
98+
{
99+
// Act
100+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
101+
102+
// Assert
103+
Assert.True(result, $"Unicode project name should be valid: {projectName}");
104+
}
105+
106+
[Theory]
107+
[InlineData("Ελληνικά", true)] // Greek
108+
[InlineData("עברית", true)] // Hebrew
109+
[InlineData("हिन्दी", true)] // Hindi
110+
[InlineData("ไทย", true)] // Thai
111+
[InlineData("한국어", true)] // Korean
112+
[InlineData("Türkçe", true)] // Turkish
113+
[InlineData("Português", true)] // Portuguese with accent
114+
[InlineData("Français", true)] // French with accent
115+
[InlineData("Español", true)] // Spanish with accent
116+
[InlineData("Deutsch", true)] // German
117+
public void IsProjectNameValid_VariousLanguages_ReturnsTrue(string projectName, bool expected)
118+
{
119+
// Act
120+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
121+
122+
// Assert
123+
Assert.Equal(expected, result);
124+
}
125+
126+
[Theory]
127+
[InlineData("Test123-Project_Name.v2")] // Complex valid with all allowed characters
128+
[InlineData("A1-B2_C3.D4")] // Mixed with separators
129+
[InlineData("项目-测试_版本.1")] // Unicode with separators
130+
public void IsProjectNameValid_ComplexValidNames_ReturnsTrue(string projectName)
131+
{
132+
// Act
133+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
134+
135+
// Assert
136+
Assert.True(result, $"Complex valid project name should be valid: {projectName}");
137+
}
138+
139+
[Theory]
140+
[InlineData("Test..Name")] // Double dot
141+
[InlineData("Test--Name")] // Double dash
142+
[InlineData("Test__Name")] // Double underscore
143+
public void IsProjectNameValid_ConsecutiveSpecialChars_ReturnsTrue(string projectName)
144+
{
145+
// These should be valid as the spec doesn't prohibit consecutive allowed characters
146+
// Act
147+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
148+
149+
// Assert
150+
Assert.True(result, $"Consecutive allowed characters should be valid: {projectName}");
151+
}
152+
153+
[Theory]
154+
[InlineData("My/Project")] // Forward slash in middle
155+
[InlineData("/MyProject")] // Forward slash at start
156+
[InlineData("MyProject/")] // Forward slash at end
157+
[InlineData("My\\Project")] // Backslash in middle
158+
[InlineData("\\MyProject")] // Backslash at start
159+
[InlineData("MyProject\\")] // Backslash at end
160+
[InlineData("My/Project/Name")] // Multiple forward slashes
161+
[InlineData("My\\Project\\Name")] // Multiple backslashes
162+
[InlineData("My/Project\\Name")] // Mixed path separators
163+
public void IsProjectNameValid_PathSeparators_ReturnsFalse(string projectName)
164+
{
165+
// Act
166+
var result = ProjectNameValidator.IsProjectNameValid(projectName);
167+
168+
// Assert
169+
Assert.False(result, $"Project name with path separators should be invalid: {projectName}");
170+
}
171+
}

0 commit comments

Comments
 (0)