Skip to content

Commit ce8705a

Browse files
Mpdreamzclaude
andauthored
Replace Path.Combine with Path.Join (#2997)
* Replace Path.Combine with Path.Join across all C# files Path.Join is preferred over Path.Combine because it does not treat absolute path segments as rooted, preventing potential path traversal issues. See dotnet/runtime#24263 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix IDE0370: remove unnecessary null-forgiving operators in ChangelogRemoveService Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address CodeRabbit review: path traversal hardening, URL construction fixes, and shared utilities - Add UrlPath static class for URL-safe path joining (always '/', normalises backslashes) - Add Paths.ValidateSinglePathSegment shared utility; remove duplicated copies - GitLinkIndexReader: replace IsPathRooted check with full-path containment validation to block '..' traversal segments - DetectionRuleFile: handle Windows path separator in relative path prefix trim - HtmlWriter: use UrlPath.Join/JoinUrl instead of Path.Join for URL construction - RepositorySourcesFetcher: use Paths.ValidateSinglePathSegment before joining repo.Name into filesystem paths - DocumentationWebHost: verify slug-derived path stays under ApiPath - StaticWebHost: verify slug-derived path stays under content root - SiteNavigationTests: remove redundant nested Path.Join call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix import ordering in HtmlWriter.cs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Forward appendRepositoryName and assumeCloned on CloneRef retry Retry calls were dropping non-default arguments, causing retries to resolve a different checkout folder than the initial attempt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 61dd902 commit ce8705a

104 files changed

Lines changed: 894 additions & 838 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Elastic.ApiExplorer/OpenApiGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ IFileInfo OutputFile(INavigationItem currentNavigation)
316316
{
317317
const string indexHtml = "index.html";
318318
var fileName = Regex.Replace(currentNavigation.Url + "/" + indexHtml, $"^{context.UrlPathPrefix}", string.Empty);
319-
var fileInfo = _writeFileSystem.FileInfo.New(Path.Combine(context.OutputDirectory.FullName, fileName.Trim('/')));
319+
var fileInfo = _writeFileSystem.FileInfo.New(Path.Join(context.OutputDirectory.FullName, fileName.Trim('/')));
320320
return fileInfo;
321321
}
322322
}

src/Elastic.Codex/Building/CodexBuildService.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ public async Task<CodexBuildResult> BuildAll(
150150

151151
// Build output path: {outputDir}/{sitePrefix}/r/{repoName} or {outputDir}/r/{repoName} if no prefix
152152
var outputPath = string.IsNullOrEmpty(sitePrefix)
153-
? fileSystem.Path.Combine(context.OutputDirectory.FullName, "r", repoName)
154-
: fileSystem.Path.Combine(context.OutputDirectory.FullName, sitePrefix, "r", repoName);
153+
? fileSystem.Path.Join(context.OutputDirectory.FullName, "r", repoName)
154+
: fileSystem.Path.Join(context.OutputDirectory.FullName, sitePrefix, "r", repoName);
155155

156156
// Build URL path prefix: /r/{repoName} or /{sitePrefix}/r/{repoName}
157157
var pathPrefix = string.IsNullOrEmpty(sitePrefix)
@@ -311,7 +311,7 @@ private async Task OutputRedirectsAsync(CodexContext context, Dictionary<string,
311311
var uniqueRedirects = redirects
312312
.Where(x => !x.Key.TrimEnd('/').Equals(x.Value.TrimEnd('/'), StringComparison.OrdinalIgnoreCase))
313313
.ToDictionary();
314-
var redirectsFile = context.WriteFileSystem.FileInfo.New(context.WriteFileSystem.Path.Combine(context.OutputDirectory.FullName, "redirects.json"));
314+
var redirectsFile = context.WriteFileSystem.FileInfo.New(context.WriteFileSystem.Path.Join(context.OutputDirectory.FullName, "redirects.json"));
315315
_logger.LogInformation("Writing {Count} resolved redirects to {Path}", uniqueRedirects.Count, redirectsFile.FullName);
316316

317317
var redirectsJson = JsonSerializer.Serialize(uniqueRedirects, SourceGenerationContext.Default.DictionaryStringString);

src/Elastic.Codex/CodexContext.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ public CodexContext(
4545
ReadFileSystem = readFileSystem;
4646
WriteFileSystem = writeFileSystem;
4747

48-
var defaultCheckoutDirectory = Path.Combine(Paths.GitCommonRoot.FullName, ".artifacts", "codex", "clone");
48+
var defaultCheckoutDirectory = Path.Join(Paths.GitCommonRoot.FullName, ".artifacts", "codex", "clone");
4949
CheckoutDirectory = ReadFileSystem.DirectoryInfo.New(checkoutDirectory ?? defaultCheckoutDirectory);
5050

51-
var defaultOutputDirectory = Path.Combine(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "codex", "docs");
51+
var defaultOutputDirectory = Path.Join(Paths.WorkingDirectoryRoot.FullName, ".artifacts", "codex", "docs");
5252
OutputDirectory = ReadFileSystem.DirectoryInfo.New(outputDirectory ?? defaultOutputDirectory);
5353
}
5454
}

src/Elastic.Codex/CodexGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private async Task ExtractEmbeddedStaticResources(Cancel ctx)
8888
.Replace("_static.", $"_static{Path.DirectorySeparatorChar}");
8989

9090
// Output to codex's URL prefix directory (e.g., internal-docs/_static/)
91-
var outputPath = Path.Combine(
91+
var outputPath = Path.Join(
9292
_outputDirectory.FullName,
9393
context.UrlPathPrefix?.Trim('/') ?? string.Empty,
9494
path);
@@ -174,7 +174,7 @@ private IFileInfo GetOutputFile(string url)
174174
const string indexHtml = "index.html";
175175
// Keep the full URL path so file structure matches URLs
176176
var fileName = url.TrimStart('/') + "/" + indexHtml;
177-
return _writeFileSystem.FileInfo.New(Path.Combine(_outputDirectory.FullName, fileName.Trim('/')));
177+
return _writeFileSystem.FileInfo.New(Path.Join(_outputDirectory.FullName, fileName.Trim('/')));
178178
}
179179
}
180180

src/Elastic.Codex/Sourcing/CodexCloneService.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class CodexCloneService(ILoggerFactory logFactory, ILinkIndexReader linkI
3434
if (!checkoutDir.Exists)
3535
return null;
3636

37-
var snapshotFilePath = Path.Combine(checkoutDir.FullName, LinkRegistrySnapshotFileName);
37+
var snapshotFilePath = Path.Join(checkoutDir.FullName, LinkRegistrySnapshotFileName);
3838
if (!context.ReadFileSystem.File.Exists(snapshotFilePath))
3939
return null;
4040

@@ -44,7 +44,7 @@ public class CodexCloneService(ILoggerFactory logFactory, ILinkIndexReader linkI
4444
var checkouts = new List<CodexCheckout>();
4545
foreach (var subDir in checkoutDir.GetDirectories())
4646
{
47-
var gitDir = Path.Combine(subDir.FullName, ".git");
47+
var gitDir = Path.Join(subDir.FullName, ".git");
4848
if (!context.ReadFileSystem.Directory.Exists(gitDir))
4949
continue;
5050

@@ -128,7 +128,7 @@ await Parallel.ForEachAsync(
128128
if (Path.IsPathRooted(LinkRegistrySnapshotFileName))
129129
throw new InvalidOperationException($"Snapshot file name '{LinkRegistrySnapshotFileName}' must be a relative path.");
130130

131-
var snapshotFilePath = Path.Combine(context.CheckoutDirectory.FullName, LinkRegistrySnapshotFileName);
131+
var snapshotFilePath = Path.Join(context.CheckoutDirectory.FullName, LinkRegistrySnapshotFileName);
132132

133133
await context.WriteFileSystem.File.WriteAllTextAsync(
134134
snapshotFilePath,
@@ -169,7 +169,7 @@ await context.WriteFileSystem.File.WriteAllTextAsync(
169169
}
170170

171171
var repoDir = context.ReadFileSystem.DirectoryInfo.New(
172-
Path.Combine(context.CheckoutDirectory.FullName, repoName));
172+
Path.Join(context.CheckoutDirectory.FullName, repoName));
173173

174174
var gitUrl = GetGitUrl($"elastic/{repoName}");
175175
var gitRef = fetchLatest ? entry.Branch : entry.GitReference;
@@ -238,7 +238,7 @@ await context.WriteFileSystem.File.WriteAllTextAsync(
238238
{
239239
foreach (var candidate in DocsetSearchPaths)
240240
{
241-
var path = Path.Combine(repoDir.FullName, candidate);
241+
var path = Path.Join(repoDir.FullName, candidate);
242242
var file = fileSystem.FileInfo.New(path);
243243
if (file.Exists)
244244
return file;

src/Elastic.Codex/Sourcing/CodexGitRepository.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public class CodexGitRepository(ILoggerFactory logFactory, IDiagnosticsCollector
2828

2929
public void Init() => ExecIn(EnvironmentVars, "git", "init");
3030

31-
public bool IsInitialized() => Directory.Exists(Path.Combine(WorkingDirectory.FullName, ".git"));
31+
public bool IsInitialized() => Directory.Exists(Path.Join(WorkingDirectory.FullName, ".git"));
3232

3333
public void Fetch(string reference) =>
3434
ExecIn(EnvironmentVars, "git", "fetch", "--no-tags", "--prune", "--no-recurse-submodules", "--depth", "1", "origin", reference);

src/Elastic.Documentation.Configuration/Assembler/AssemblyConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static AssemblyConfiguration Deserialize(string yaml, bool skipPrivateRep
3535
&& Paths.GetSolutionDirectory() is { } solutionDir
3636
)
3737
{
38-
var docsRepositoryPath = Path.Combine(solutionDir.FullName, "docs");
38+
var docsRepositoryPath = Path.Join(solutionDir.FullName, "docs");
3939
config.ReferenceRepositories["docs-builder"] = docsContentRepository with
4040
{
4141
Skip = false,

src/Elastic.Documentation.Configuration/BuildContext.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,15 @@ public BuildContext(
103103

104104
var rootFolder = !string.IsNullOrWhiteSpace(source)
105105
? ReadFileSystem.DirectoryInfo.New(source)
106-
: ReadFileSystem.DirectoryInfo.New(Path.Combine(Paths.WorkingDirectoryRoot.FullName));
106+
: ReadFileSystem.DirectoryInfo.New(Path.Join(Paths.WorkingDirectoryRoot.FullName));
107107

108108
(DocumentationSourceDirectory, ConfigurationPath) = Paths.FindDocsFolderFromRoot(ReadFileSystem, rootFolder);
109109

110110
DocumentationCheckoutDirectory = Paths.DetermineSourceDirectoryRoot(DocumentationSourceDirectory);
111111

112112
OutputDirectory = !string.IsNullOrWhiteSpace(output)
113113
? WriteFileSystem.DirectoryInfo.New(output)
114-
: WriteFileSystem.DirectoryInfo.New(Path.Combine(rootFolder.FullName, Path.Combine(".artifacts", "docs", "html")));
114+
: WriteFileSystem.DirectoryInfo.New(Path.Join(rootFolder.FullName, Path.Join(".artifacts", "docs", "html")));
115115

116116
if (ConfigurationPath.FullName != DocumentationSourceDirectory.FullName)
117117
DocumentationSourceDirectory = ConfigurationPath.Directory!;

src/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public ConfigurationFile(DocumentationSetFile docSetFile, IDocumentationSetConte
140140
var specs = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase);
141141
foreach (var (k, v) in docSetFile.Api)
142142
{
143-
var path = Path.Combine(context.DocumentationSourceDirectory.FullName, v);
143+
var path = Path.Join(context.DocumentationSourceDirectory.FullName, v);
144144
var fi = context.ReadFileSystem.FileInfo.New(path);
145145
specs[k] = fi;
146146
}

src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public RedirectFile(IDocumentationSetContext context, IFileInfo? source = null)
1919
{
2020
var docsetConfigurationPath = context.ConfigurationPath;
2121
var redirectFileName = docsetConfigurationPath.Name.StartsWith('_') ? "_redirects.yml" : "redirects.yml";
22-
var redirectFileInfo = docsetConfigurationPath.FileSystem.FileInfo.New(Path.Combine(docsetConfigurationPath.Directory!.FullName, redirectFileName));
22+
var redirectFileInfo = docsetConfigurationPath.FileSystem.FileInfo.New(Path.Join(docsetConfigurationPath.Directory!.FullName, redirectFileName));
2323
Source = source ?? redirectFileInfo;
2424
Context = context;
2525

0 commit comments

Comments
 (0)