Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 141 additions & 6 deletions src/Persistence.Tests/Compression/PaArchivePathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,13 @@ public void ValidDirectoryPathTest(string fullPath, string expectedRelativePathW
[DataRow(@"dir1\file3 \", PaArchivePathInvalidReason.SegmentWithLeadingOrTrailingWhitespace)]
[DataRow(@"dir1\ file3 ", PaArchivePathInvalidReason.SegmentWithLeadingOrTrailingWhitespace)]
// Relative directory segments are illegal (See: Zip Path Traversal vulnerability)
[DataRow(@"parent\..\dir", PaArchivePathInvalidReason.IllegalSegment)]
[DataRow(@"malicious\..\..\..\path", PaArchivePathInvalidReason.IllegalSegment)]
[DataRow("""..\..\..\Windows\System32\drivers\etc\hosts""", PaArchivePathInvalidReason.IllegalSegment)]
[DataRow(@"./current/dir", PaArchivePathInvalidReason.IllegalSegment)]
[DataRow(@"current/./dir", PaArchivePathInvalidReason.IllegalSegment)]
[DataRow(@"illegal/win/filename.", PaArchivePathInvalidReason.IllegalSegment)] // Windows: cannot end with a period
[DataRow(@"parent\..\dir", PaArchivePathInvalidReason.RelativeSegment)]
[DataRow(@"malicious\..\..\..\path", PaArchivePathInvalidReason.RelativeSegment)]
[DataRow("""..\..\..\Windows\System32\drivers\etc\hosts""", PaArchivePathInvalidReason.RelativeSegment)]
[DataRow(@"./current/dir", PaArchivePathInvalidReason.RelativeSegment)]
[DataRow(@"current/./dir", PaArchivePathInvalidReason.RelativeSegment)]
[DataRow(@"illegal/win/filename.", PaArchivePathInvalidReason.SegmentEndsWithDot)] // Windows: cannot end with a period
[DataRow(@"illegal/win/segment./filename", PaArchivePathInvalidReason.SegmentEndsWithDot)] // Windows: cannot end with a period
// extended-length paths are not allowed:
[DataRow("""\\?\some\long\file\path""", PaArchivePathInvalidReason.InvalidPathChars)]
public void InvalidPathTest(string fullName, PaArchivePathInvalidReason expectedReason)
Expand All @@ -189,6 +190,9 @@ public void GetInvalidPathCharsTest()
invalidChars.Should().NotBeEmpty();
invalidChars.Should().Contain(Path.GetInvalidPathChars(), "because all chars returned by Path.GetInvalidPathChars for the current platform should be included");
invalidChars.Should().Contain(Path.GetInvalidFileNameChars().Where(c => !PaArchivePath.GetAllSeparatorChars().Contains(c)), "because all chars returned by Path.GetInvalidFileNameChars (except for separator chars) for the current platform should be included");
invalidChars.Should().Contain('\b', "BS char should not be allowed");
invalidChars.Should().Contain('\t', "HT char should not be allowed");
invalidChars.Should().Contain('\x007F', "DEL char should not be allowed, even though the Path.GetInvalidPathChars doesn't have it");
PaArchivePath.GetInvalidPathChars().Should().NotBeSameAs(invalidChars, "because we should be returning a new array instance each time to prevent callers from modifying the cached array");
}

Expand All @@ -202,6 +206,137 @@ public void GetAllSeparatorCharsTest()
PaArchivePath.GetAllSeparatorChars().Should().NotBeSameAs(allSeparatorChars, "because we should be returning a new array instance each time to prevent callers from modifying the cached array");
}

[TestMethod]
public void GetInvalidSegmentCharsTest()
{
var invalidSegmentChars = PaArchivePath.GetInvalidSegmentChars();
invalidSegmentChars.Should().NotBeEmpty();
invalidSegmentChars.Should().Contain(PaArchivePath.GetInvalidPathChars(), "because all chars from GetInvalidPathChars should be included");
invalidSegmentChars.Should().Contain(PaArchivePath.GetAllSeparatorChars(), "because separator chars delimit segments and are therefore not valid within a segment");
invalidSegmentChars.Should().Contain(Path.GetInvalidPathChars(), "because all chars returned by Path.GetInvalidPathChars for the current platform should be included");
invalidSegmentChars.Should().Contain(Path.GetInvalidFileNameChars(), "because all chars returned by Path.GetInvalidFileNameChars for the current platform should be included");
PaArchivePath.GetInvalidSegmentChars().Should().NotBeSameAs(invalidSegmentChars, "because we should be returning a new array instance each time to prevent callers from modifying the cached array");
}

[TestMethod]
// Valid segments
[DataRow("file.txt")]
[DataRow("SomeName")]
[DataRow(".hidden")]
[DataRow("some-dir_with555.common.chars")]
// Whitespace only
[DataRow("", PaArchivePathInvalidReason.WhitespaceOnlySegment)]
[DataRow(" ", PaArchivePathInvalidReason.WhitespaceOnlySegment)]
[DataRow(" ", PaArchivePathInvalidReason.WhitespaceOnlySegment)]
[DataRow("\t", PaArchivePathInvalidReason.WhitespaceOnlySegment)]
// Invalid chars (including separator chars)
[DataRow("foo<bar", PaArchivePathInvalidReason.InvalidPathChars)]
[DataRow("foo*bar", PaArchivePathInvalidReason.InvalidPathChars)]
[DataRow("ascii-\0-null", PaArchivePathInvalidReason.InvalidPathChars)]
[DataRow("has/separator", PaArchivePathInvalidReason.InvalidPathChars)]
[DataRow(@"has\separator", PaArchivePathInvalidReason.InvalidPathChars)]
// Leading/trailing whitespace
[DataRow(" file", PaArchivePathInvalidReason.SegmentWithLeadingOrTrailingWhitespace)]
[DataRow("file ", PaArchivePathInvalidReason.SegmentWithLeadingOrTrailingWhitespace)]
[DataRow(" file ", PaArchivePathInvalidReason.SegmentWithLeadingOrTrailingWhitespace)]
// Illegal segments
[DataRow("..", PaArchivePathInvalidReason.RelativeSegment)]
[DataRow(".", PaArchivePathInvalidReason.RelativeSegment)]
[DataRow("ends-with-dot.", PaArchivePathInvalidReason.SegmentEndsWithDot)]
public void TryValidateSegmentTest(string segment, PaArchivePathInvalidReason? expectedReason = null)
{
var expectIsValid = expectedReason is null;
PaArchivePath.TryValidateSegment(segment, out var reason)
.Should().Be(expectIsValid);
reason.Should().Be(expectedReason);

PaArchivePath.IsValidSegment(segment)
.Should().Be(expectIsValid, "because IsValidSegment should agree with TryValidateSegment");
}

[TestMethod]
// Already valid (returned as-is)
[DataRow("file.txt", "file.txt")]
[DataRow(".hidden", ".hidden")]
[DataRow("foo- -bar.txt", "foo- -bar.txt")]
[DataRow("foo-éñü-bar.txt", "foo-éñü-bar.txt")]
[DataRow("foo-あア-bar.txt", "foo-あア-bar.txt")]
[DataRow("foo-中文-bar.txt", "foo-中文-bar.txt")]
// Invalid chars are removed
[DataRow("foo<bar", "foobar")]
[DataRow("foo*bar", "foobar")]
[DataRow("foo\tbar", "foobar")]
[DataRow("has/separator", "hasseparator")]
[DataRow(@"has\separator", "hasseparator")]
// Whitespace trimming
[DataRow(" file ", "file")]
[DataRow(" file ", "file")]
[DataRow(" \t file \t ", "file")]
[DataRow(" fi le ", "fi le")]
// Results in empty → false
[DataRow("", null)]
[DataRow(" ", null)]
[DataRow("\t", null)]
[DataRow("/", null)]
[DataRow("<>|", null)]
// Illegal segments
[DataRow(".", null)]
[DataRow("..", null)]
[DataRow("...", null)]
[DataRow(" . ", null)]
[DataRow(" .. ", null)]
[DataRow(" ... ", null)]
[DataRow("ends-with-dot.", "ends-with-dot")]
[DataRow("ends-with-dots...", "ends-with-dots")]
public void TryMakeValidSegmentTest(string segment, string? expectedValidSegment)
{
PaArchivePath.TryMakeValidSegment(segment, out var validSegment)
.Should().Be(expectedValidSegment is not null);
validSegment.Should().Be(expectedValidSegment);
}

[TestMethod]
// Already valid (returned as-is)
[DataRow("file.txt", "file.txt")]
[DataRow(".hidden", ".hidden")]
[DataRow("foo- -bar.txt", "foo- -bar.txt")]
[DataRow("foo-éñü-bar.txt", "foo-éñü-bar.txt")]
[DataRow("foo-あア-bar.txt", "foo-あア-bar.txt")]
[DataRow("foo-中文-bar.txt", "foo-中文-bar.txt")]
// Invalid chars are replaced
[DataRow("foo<bar", "foo_bar")]
[DataRow("foo*bar", "foo_bar")]
[DataRow("foo\tbar", "foo_bar")]
[DataRow("has/separator", "has_separator")]
[DataRow(@"has\separator", "has_separator")]
// Whitespace replaced (no longer trimmed when a replacement char is supplied)
[DataRow(" file ", "_file_")]
[DataRow(" file", "__file")]
[DataRow(" file ", "__file__")]
[DataRow("file ", "file__")]
[DataRow(" \t file \t ", "___file___")]
[DataRow(" fi le ", "_fi le_")]
// Results in empty/replacement-only
[DataRow("", null)]
[DataRow(" ", "_")]
[DataRow("\t", "_")]
[DataRow("/", "_")]
[DataRow("<>|", "___")]
// Illegal segments
[DataRow(".", "_")]
[DataRow("..", "._")]
[DataRow("...", ".._")]
[DataRow(" . ", "_._")]
[DataRow(" .. ", "_.._")]
[DataRow(" ... ", "_..._")]
[DataRow("ends-with-dot.", "ends-with-dot_")]
[DataRow("ends-with-dots...", "ends-with-dots.._")]
public void TryMakeValidSegment_WithReplacementCharTest(string segment, string? expectedValidSegment)
{
PaArchivePath.TryMakeValidSegment(segment, out var validSegment, replacementChar: '_')
.Should().Be(expectedValidSegment is not null);
validSegment.Should().Be(expectedValidSegment);
}
[TestMethod]
public void GetHashCodeTest()
{
Expand Down
118 changes: 0 additions & 118 deletions src/Persistence.Tests/MsApp/MsappArchiveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,122 +133,4 @@ private static void SaveNewMinMsappWithHeaderOnly(MemoryStream archiveStream, st
var headerFilePath = Path.Combine("_TestData", "headers", headerFileName);
writeToArchive.CreateEntryFromFile(headerFilePath, "Header.json");
}

[TestMethod]
[DataRow(":%/\\?!", false, DisplayName = "Unsafe chars only")]
[DataRow(" :%/\\ ?! ", false, DisplayName = "Unsafe and whitespace chars only")]
[DataRow("", false, DisplayName = "empty string")]
[DataRow(" ", false, DisplayName = "whitespace chars only")]
[DataRow("Foo.Bar", true, "Foo.Bar")]
[DataRow(" Foo Bar ", true, "Foo Bar", DisplayName = "with leading/trailing whitespace")]
[DataRow("Foo:%/\\-?!Bar", true, "Foo-Bar")]
public void TryMakeSafeForEntryPathSegmentWithDefaultReplacementTests(string unsafeName, bool expectedReturn, string? expectedSafeName = null)
{
MsappArchive.TryMakeSafeForEntryPathSegment(unsafeName, out var safeName).Should().Be(expectedReturn);
if (expectedReturn)
{
safeName.ShouldNotBeNull();
if (expectedSafeName != null)
{
safeName.Should().Be(expectedSafeName);
}
}
else
{
safeName.Should().BeNull();
}
}

[TestMethod]
[DataRow(":%/\\?!", true, "______", DisplayName = "Unsafe chars only")]
[DataRow(" :%/\\ ?! ", true, "____ __", DisplayName = "Unsafe and whitespace chars only")]
[DataRow("", false, DisplayName = "empty string")]
[DataRow(" ", false, DisplayName = "whitespace chars only")]
[DataRow("Foo.Bar", true, "Foo.Bar")]
[DataRow(" Foo Bar ", true, "Foo Bar", DisplayName = "with leading/trailing whitespace")]
[DataRow("Foo:%/\\-?!Bar", true, "Foo____-__Bar")]
public void TryMakeSafeForEntryPathSegmentWithUnderscoreReplacementTests(string unsafeName, bool expectedReturn, string? expectedSafeName = null)
{
MsappArchive.TryMakeSafeForEntryPathSegment(unsafeName, out var safeName, unsafeCharReplacementText: "_").Should().Be(expectedReturn);
if (expectedReturn)
{
safeName.ShouldNotBeNull();
if (expectedSafeName != null)
{
safeName.Should().Be(expectedSafeName);
}
}
else
{
safeName.Should().BeNull();
}
}

[TestMethod]
public void TryMakeSafeForEntryPathSegmentWhereInputContainsPathSeparatorCharsTests()
{
MsappArchive.TryMakeSafeForEntryPathSegment("Foo\\Bar.pa.yaml", out var safeName).Should().BeTrue();
safeName.Should().Be("FooBar.pa.yaml");
MsappArchive.TryMakeSafeForEntryPathSegment("Foo/Bar.pa.yaml", out safeName).Should().BeTrue();
safeName.Should().Be("FooBar.pa.yaml");

// with replacement
MsappArchive.TryMakeSafeForEntryPathSegment("Foo\\Bar.pa.yaml", out safeName, unsafeCharReplacementText: "_").Should().BeTrue();
safeName.Should().Be("Foo_Bar.pa.yaml");
MsappArchive.TryMakeSafeForEntryPathSegment("Foo/Bar.pa.yaml", out safeName, unsafeCharReplacementText: "-").Should().BeTrue();
safeName.Should().Be("Foo-Bar.pa.yaml");
}

[TestMethod]
public void TryMakeSafeForEntryPathSegmentWhereInputContainsInvalidPathCharTests()
{
var invalidChars = Path.GetInvalidPathChars()
.Union(Path.GetInvalidFileNameChars());
foreach (var c in invalidChars)
{
// Default behavior should remove invalid chars
MsappArchive.TryMakeSafeForEntryPathSegment($"Foo{c}Bar.pa.yaml", out var safeName).Should().BeTrue();
safeName.Should().Be("FooBar.pa.yaml");

// Replacement char should be used for invalid chars
MsappArchive.TryMakeSafeForEntryPathSegment($"Foo{c}Bar.pa.yaml", out safeName, unsafeCharReplacementText: "_").Should().BeTrue();
safeName.Should().Be("Foo_Bar.pa.yaml");

// When input results in only whitespace or empty, return value should be false
MsappArchive.TryMakeSafeForEntryPathSegment($"{c}", out _).Should().BeFalse("because safe segment is empty string");
MsappArchive.TryMakeSafeForEntryPathSegment($" {c} ", out _).Should().BeFalse("because safe segment is whitespace");
MsappArchive.TryMakeSafeForEntryPathSegment($"{c} {c}", out _).Should().BeFalse("because safe segment is whitespace");
}
}

[TestMethod]
public void IsSafeForEntryPathSegmentTests()
{
MsappArchive.IsSafeForEntryPathSegment("Foo.pa.yaml").Should().BeTrue();

// Path separator chars should not be used for path segments
MsappArchive.IsSafeForEntryPathSegment("Foo/Bar.pa.yaml").Should().BeFalse("separator chars should not be used for path segments");
MsappArchive.IsSafeForEntryPathSegment("/Foo.pa.yaml").Should().BeFalse("separator chars should not be used for path segments");
MsappArchive.IsSafeForEntryPathSegment("Foo\\Bar.pa.yaml").Should().BeFalse("separator chars should not be used for path segments");
MsappArchive.IsSafeForEntryPathSegment("\\Foo.pa.yaml").Should().BeFalse("separator chars should not be used for path segments");

MsappArchive.IsSafeForEntryPathSegment("Foo/\t.pa.yaml").Should().BeFalse("control chars should not be allowed");

// Currently, chars outside of ascii range are not allowed
MsappArchive.IsSafeForEntryPathSegment("Foo/éñü.pa.yaml").Should().BeFalse("latin chars are currently not allowed");
MsappArchive.IsSafeForEntryPathSegment("Foo/あア.pa.yaml").Should().BeFalse("Japanese chars are currently not allowed");
MsappArchive.IsSafeForEntryPathSegment("Foo/中文.pa.yaml").Should().BeFalse("CJK chars are currently not allowed");
}

[TestMethod]
public void IsSafeForEntryPathSegmentShouldNotAllowInvalidPathCharsTests()
{
var invalidChars = Path.GetInvalidPathChars()
.Union(Path.GetInvalidFileNameChars());

foreach (var c in invalidChars)
{
MsappArchive.IsSafeForEntryPathSegment($"Foo{c}Bar.pa.yaml").Should().BeFalse($"Invalid char '{c}' should not be allowed for path segments");
}
}
}
Loading
Loading