-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
187 lines (169 loc) · 7.63 KB
/
Copy pathProgram.cs
File metadata and controls
187 lines (169 loc) · 7.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace ShellUI.SafelistGenerator;
/* Scans .razor and .cs files for Tailwind utility classes and emits:
- wwwroot/shellui-classes.txt (committed, used for drift detection + demo build)
- build/ShellUI.Components.targets (NuGet auto-imports; embeds the list inline so we don't
depend on NuGet extracting a separate data file — behavior varies by client) */
public static class Program
{
public static int Main(string[] args)
{
if (args.Length != 3)
{
Console.Error.WriteLine("usage: ShellUI.SafelistGenerator <razor-dir> <txt-out> <targets-out>");
return 1;
}
var razorDir = args[0];
var txtOut = args[1];
var targetsOut = args[2];
if (!Directory.Exists(razorDir))
{
Console.Error.WriteLine($"razor directory not found: {razorDir}");
return 2;
}
var (razorFiles, csFiles) = EnumerateSources(razorDir);
var classes = GenerateSafelist(razorFiles.Concat(csFiles));
Directory.CreateDirectory(Path.GetDirectoryName(txtOut)!);
File.WriteAllLines(txtOut, classes);
Directory.CreateDirectory(Path.GetDirectoryName(targetsOut)!);
File.WriteAllText(targetsOut, BuildTargetsFileContent(classes));
Console.WriteLine($"wrote {classes.Count} classes from {razorFiles.Length} razor + {csFiles.Length} cs files");
Console.WriteLine($" txt: {txtOut}");
Console.WriteLine($" targets: {targetsOut}");
return 0;
}
// Shared with tests so the drift check scans the same file set as the CLI.
public static (string[] razorFiles, string[] csFiles) EnumerateSources(string razorDir)
{
var razorFiles = Directory.GetFiles(razorDir, "*.razor", SearchOption.AllDirectories);
var componentsRoot = Path.GetDirectoryName(razorDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
?? razorDir;
var csFiles = Directory.GetFiles(componentsRoot, "*.cs", SearchOption.AllDirectories)
.Where(p => !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")
&& !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}"))
.ToArray();
return (razorFiles, csFiles);
}
public static SortedSet<string> GenerateSafelist(IEnumerable<string> sourceFilePaths)
{
var classes = new SortedSet<string>(StringComparer.Ordinal);
foreach (var file in sourceFilePaths)
{
var text = File.ReadAllText(file);
if (file.EndsWith(".razor", StringComparison.OrdinalIgnoreCase))
{
ExtractClasses(text, classes);
}
else if (file.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
{
ExtractFromCSharp(text, classes);
}
}
return classes;
}
// .cs literals need filtering — without it, JS/CSS/error-message strings pollute the safelist.
private static void ExtractFromCSharp(string content, SortedSet<string> sink)
{
foreach (Match lit in StringLiteralRegex.Matches(content))
{
var value = lit.Groups["lit"].Value;
if (!LooksLikeClassList(value)) continue;
HarvestTokens(value, sink);
}
}
private static bool LooksLikeClassList(string s)
{
if (s.Length == 0 || s.Length > 500) return false;
// `=` outside `[…]` implies key=value pairs, not class strings.
if (s.Contains("://") || s.Contains('<') || s.Contains('>') || s.Contains('{')
|| s.Contains(';') || s.Contains('=') && !s.Contains('[')
|| s.Contains('\n')) return false;
var tokens = s.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length == 0) return false;
var anyUtility = false;
foreach (var tok in tokens)
{
if (tok.Length < 2) return false;
var t = tok.TrimEnd(',', ';', ')', '"');
if (t.Length < 2) return false;
if (!char.IsLower(t[0]) && t[0] != '[' && t[0] != '!') return false;
foreach (var c in t)
{
if (!(char.IsLetterOrDigit(c) || c is '-' or '_' or ':' or '/' or '.' or '[' or ']'
or '(' or ')' or '=' or '&' or ',' or '!' or '%' or '#' or '@' or '*'))
return false;
}
if (t.Contains('-') || t.Contains(':') || t.Contains('[') || t.Contains('/'))
anyUtility = true;
}
return anyUtility;
}
// XML comments in the generated .targets must not contain `--` (XML 1.0 spec); MSBuild on
// Linux .NET 10.301+ rejects the file with MSB4024. Keep the emitted comment text safe.
public static string BuildTargetsFileContent(IEnumerable<string> classes)
{
var sb = new StringBuilder();
sb.AppendLine("<Project>");
sb.AppendLine(" <!-- AUTO-GENERATED by tools/ShellUI.SafelistGenerator. Do not edit by hand. -->");
sb.AppendLine(" <ItemGroup>");
foreach (var cls in classes)
{
sb.Append(" <ShellUISafelistEntry Include=\"");
sb.Append(XmlEscape(cls));
sb.AppendLine("\" />");
}
sb.AppendLine(" </ItemGroup>");
sb.AppendLine();
sb.AppendLine(" <Target Name=\"WriteShellUISafelist\"");
sb.AppendLine(" BeforeTargets=\"BeforeBuild;PrepareForBuild\"");
sb.AppendLine(" Condition=\"'$(SkipShellUISafelist)' != 'true'\">");
sb.AppendLine(" <MakeDir Directories=\"$(MSBuildProjectDirectory)/wwwroot\" Condition=\"!Exists('$(MSBuildProjectDirectory)/wwwroot')\" />");
sb.AppendLine(" <WriteLinesToFile File=\"$(MSBuildProjectDirectory)/wwwroot/shellui-classes.txt\"");
sb.AppendLine(" Lines=\"@(ShellUISafelistEntry)\"");
sb.AppendLine(" Overwrite=\"true\" />");
sb.AppendLine(" </Target>");
sb.AppendLine("</Project>");
return sb.ToString();
}
private static string XmlEscape(string s)
{
return s.Replace("&", "&")
.Replace("\"", """)
.Replace("<", "<")
.Replace(">", ">");
}
private static readonly Regex ClassAttributeRegex = new(
"class\\s*=\\s*\"(?<value>[^\"]*)\"",
RegexOptions.Compiled);
private static readonly Regex StringLiteralRegex = new(
"\"(?<lit>[^\"]*)\"",
RegexOptions.Compiled);
private static void ExtractClasses(string content, SortedSet<string> sink)
{
foreach (Match attr in ClassAttributeRegex.Matches(content))
{
var value = attr.Groups["value"].Value;
HarvestTokens(value, sink);
foreach (Match lit in StringLiteralRegex.Matches(value))
{
HarvestTokens(lit.Groups["lit"].Value, sink);
}
}
}
private static void HarvestTokens(string text, SortedSet<string> sink)
{
var tokens = text.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var raw in tokens)
{
var token = raw.Trim();
if (token.Length < 2) continue;
if (!char.IsLower(token[0])) continue;
if (!token.Contains('-') && !token.Contains(':') && !token.Contains('[') && !token.Contains('/')) continue;
token = token.TrimEnd(',', ';', ')', '"');
if (token.Length < 2) continue;
sink.Add(token);
}
}
}