Skip to content

Commit 8ab21b0

Browse files
authored
PR #593: Simplify DropPathRoot and fix out of bounds issue
* Simplify DropPathRoot and fix out of bounds issue * prevent throwing in DropPathRoot and add tests * make path tests platform-specific testing for UNC paths on *nix does not really make sense * handle .NET < 4.6.2 special cases
1 parent 1493f69 commit 8ab21b0

File tree

6 files changed

+135
-69
lines changed

6 files changed

+135
-69
lines changed

src/ICSharpCode.SharpZipLib/Core/PathUtils.cs

Lines changed: 15 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System;
12
using System.IO;
3+
using System.Linq;
24

35
namespace ICSharpCode.SharpZipLib.Core
46
{
@@ -12,51 +14,21 @@ public static class PathUtils
1214
/// </summary>
1315
/// <param name="path">A <see cref="string"/> containing path information.</param>
1416
/// <returns>The path with the root removed if it was present; path otherwise.</returns>
15-
/// <remarks>Unlike the <see cref="System.IO.Path"/> class the path isn't otherwise checked for validity.</remarks>
1617
public static string DropPathRoot(string path)
1718
{
18-
string result = path;
19-
20-
if (!string.IsNullOrEmpty(path))
21-
{
22-
if ((path[0] == '\\') || (path[0] == '/'))
23-
{
24-
// UNC name ?
25-
if ((path.Length > 1) && ((path[1] == '\\') || (path[1] == '/')))
26-
{
27-
int index = 2;
28-
int elements = 2;
29-
30-
// Scan for two separate elements \\machine\share\restofpath
31-
while ((index <= path.Length) &&
32-
(((path[index] != '\\') && (path[index] != '/')) || (--elements > 0)))
33-
{
34-
index++;
35-
}
36-
37-
index++;
38-
39-
if (index < path.Length)
40-
{
41-
result = path.Substring(index);
42-
}
43-
else
44-
{
45-
result = "";
46-
}
47-
}
48-
}
49-
else if ((path.Length > 1) && (path[1] == ':'))
50-
{
51-
int dropCount = 2;
52-
if ((path.Length > 2) && ((path[2] == '\\') || (path[2] == '/')))
53-
{
54-
dropCount = 3;
55-
}
56-
result = result.Remove(0, dropCount);
57-
}
58-
}
59-
return result;
19+
var invalidChars = Path.GetInvalidPathChars();
20+
// If the first character after the root is a ':', .NET < 4.6.2 throws
21+
var cleanRootSep = path.Length >= 3 && path[1] == ':' && path[2] == ':';
22+
23+
// Replace any invalid path characters with '_' to prevent Path.GetPathRoot from throwing.
24+
// Only pass the first 258 (should be 260, but that still throws for some reason) characters
25+
// as .NET < 4.6.2 throws on longer paths
26+
var cleanPath = new string(path.Take(258)
27+
.Select( (c, i) => invalidChars.Contains(c) || (i == 2 && cleanRootSep) ? '_' : c).ToArray());
28+
29+
var stripLength = Path.GetPathRoot(cleanPath).Length;
30+
while (path.Length > stripLength && (path[stripLength] == '/' || path[stripLength] == '\\')) stripLength++;
31+
return path.Substring(stripLength);
6032
}
6133

6234
/// <summary>

test/ICSharpCode.SharpZipLib.Tests/Core/CoreTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using ICSharpCode.SharpZipLib.Core;
23
using NUnit.Framework;
34

@@ -54,5 +55,51 @@ public void ValidFilter()
5455
Assert.IsFalse(NameFilter.IsValidFilterExpression(@"\,)"));
5556
Assert.IsFalse(NameFilter.IsValidFilterExpression(@"[]"));
5657
}
58+
59+
// Use a shorter name wrapper to make tests more legible
60+
private static string DropRoot(string s) => PathUtils.DropPathRoot(s);
61+
62+
[Test]
63+
[Category("Core")]
64+
[Platform("Win")]
65+
public void DropPathRoot_Windows()
66+
{
67+
Assert.AreEqual("file.txt", DropRoot(@"\\server\share\file.txt"));
68+
Assert.AreEqual("file.txt", DropRoot(@"c:\file.txt"));
69+
Assert.AreEqual(@"subdir with spaces\file.txt", DropRoot(@"z:\subdir with spaces\file.txt"));
70+
Assert.AreEqual("", DropRoot(@"\\server\share\"));
71+
Assert.AreEqual(@"server\share\file.txt", DropRoot(@"\server\share\file.txt"));
72+
Assert.AreEqual(@"path\file.txt", DropRoot(@"\\server\share\\path\file.txt"));
73+
}
74+
75+
[Test]
76+
[Category("Core")]
77+
[Platform(Exclude="Win")]
78+
public void DropPathRoot_Posix()
79+
{
80+
Assert.AreEqual("file.txt", DropRoot("/file.txt"));
81+
Assert.AreEqual(@"tmp/file.txt", DropRoot(@"/tmp/file.txt"));
82+
Assert.AreEqual(@"tmp\file.txt", DropRoot(@"\tmp\file.txt"));
83+
Assert.AreEqual(@"tmp/file.txt", DropRoot(@"\tmp/file.txt"));
84+
Assert.AreEqual(@"tmp\file.txt", DropRoot(@"/tmp\file.txt"));
85+
Assert.AreEqual("", DropRoot("/"));
86+
87+
}
88+
89+
[Test]
90+
[TestCase(@"c:\file:+/")]
91+
[TestCase(@"c:\file*?")]
92+
[TestCase("c:\\file|\"")]
93+
[TestCase(@"c:\file<>")]
94+
[TestCase(@"c:file")]
95+
[TestCase(@"c::file")]
96+
[TestCase(@"c:?file")]
97+
[TestCase(@"c:+file")]
98+
[TestCase(@"cc:file")]
99+
[Category("Core")]
100+
public void DropPathRoot_DoesNotThrowForInvalidPath(string path)
101+
{
102+
Assert.DoesNotThrow(() => Console.WriteLine(PathUtils.DropPathRoot(path)));
103+
}
57104
}
58105
}

test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ internal static class SevenZipHelper
1717
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "7-Zip", "7z.exe"),
1818
};
1919

20-
public static bool TryGet7zBinPath(out string path7z)
20+
private static bool TryGet7zBinPath(out string path7z)
2121
{
2222
var runTimeLimit = TimeSpan.FromSeconds(3);
2323

@@ -77,12 +77,25 @@ internal static void VerifyZipWith7Zip(Stream zipStream, string password)
7777
zipStream.CopyTo(fs);
7878
}
7979

80-
var p = Process.Start(path7z, $"t -p{password} \"{fileName}\"");
80+
var p = Process.Start(new ProcessStartInfo(path7z, $"t -p{password} \"{fileName}\"")
81+
{
82+
RedirectStandardOutput = true,
83+
RedirectStandardError = true,
84+
UseShellExecute = false,
85+
});
86+
87+
if (p == null)
88+
{
89+
Assert.Inconclusive("Failed to start 7z process. Skipping!");
90+
}
8191
if (!p.WaitForExit(2000))
8292
{
8393
Assert.Warn("Timed out verifying zip file!");
8494
}
8595

96+
TestContext.Out.Write(p.StandardOutput.ReadToEnd());
97+
var errors = p.StandardError.ReadToEnd();
98+
Assert.IsEmpty(errors, "7z reported errors");
8699
Assert.AreEqual(0, p.ExitCode, "Archive verification failed");
87100
}
88101
finally

test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@ public static class Utils
1313
public static int DummyContentLength = 16;
1414

1515
private static Random random = new Random();
16+
17+
/// <summary>
18+
/// Returns the system root for the current platform (usually c:\ for windows and / for others)
19+
/// </summary>
20+
public static string SystemRoot { get; } =
21+
Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData));
1622

1723
private static void Compare(byte[] a, byte[] b)
1824
{
25+
1926
if (a == null)
2027
{
2128
throw new ArgumentNullException(nameof(a));

test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public void NullStreamDetected()
2323

2424
try
2525
{
26+
// ReSharper disable once ExpressionIsAlwaysNull
2627
bad = new ZipFile(nullStream);
2728
}
2829
catch
@@ -439,17 +440,21 @@ public void AddAndDeleteEntriesMemory()
439440
f.IsStreamOwner = false;
440441

441442
f.BeginUpdate(new MemoryArchiveStorage());
442-
f.Add(new StringMemoryDataSource("Hello world"), @"z:\a\a.dat");
443+
f.Add(new StringMemoryDataSource("Hello world"), Utils.SystemRoot + @"a\a.dat");
443444
f.Add(new StringMemoryDataSource("Another"), @"\b\b.dat");
444445
f.Add(new StringMemoryDataSource("Mr C"), @"c\c.dat");
445446
f.Add(new StringMemoryDataSource("Mrs D was a star"), @"d\d.dat");
446447
f.CommitUpdate();
447448
Assert.IsTrue(f.TestArchive(true));
449+
foreach (ZipEntry entry in f)
450+
{
451+
Console.WriteLine($" - {entry.Name}");
452+
}
448453
}
449454

450455
byte[] master = memStream.ToArray();
451456

452-
TryDeleting(master, 4, 1, @"z:\a\a.dat");
457+
TryDeleting(master, 4, 1, Utils.SystemRoot + @"a\a.dat");
453458
TryDeleting(master, 4, 1, @"\a\a.dat");
454459
TryDeleting(master, 4, 1, @"a/a.dat");
455460

test/ICSharpCode.SharpZipLib.Tests/Zip/ZipNameTransformHandling.cs

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using NUnit.Framework;
44
using System;
55
using System.IO;
6+
using ICSharpCode.SharpZipLib.Tests.TestSupport;
67

78
namespace ICSharpCode.SharpZipLib.Tests.Zip
89
{
@@ -16,8 +17,6 @@ public void Basic()
1617
var t = new ZipNameTransform();
1718

1819
TestFile(t, "abcdef", "abcdef");
19-
TestFile(t, @"\\uncpath\d1\file1", "file1");
20-
TestFile(t, @"C:\absolute\file2", "absolute/file2");
2120

2221
// This is ignored but could be converted to 'file3'
2322
TestFile(t, @"./file3", "./file3");
@@ -28,50 +27,73 @@ public void Basic()
2827

2928
// Trick filenames.
3029
TestFile(t, @".....file3", ".....file3");
30+
}
31+
32+
[Test]
33+
[Category("Zip")]
34+
[Platform("Win")]
35+
public void Basic_Windows()
36+
{
37+
var t = new ZipNameTransform();
38+
TestFile(t, @"\\uncpath\d1\file1", "file1");
39+
TestFile(t, @"C:\absolute\file2", "absolute/file2");
40+
3141
TestFile(t, @"c::file", "_file");
3242
}
43+
44+
[Test]
45+
[Category("Zip")]
46+
[Platform(Exclude="Win")]
47+
public void Basic_Posix()
48+
{
49+
var t = new ZipNameTransform();
50+
TestFile(t, @"backslash_path\file1", "backslash_path/file1");
51+
TestFile(t, "/absolute/file2", "absolute/file2");
52+
53+
TestFile(t, @"////////:file", "_file");
54+
}
3355

3456
[Test]
3557
public void TooLong()
3658
{
3759
var zt = new ZipNameTransform();
38-
var veryLong = new string('x', 65536);
39-
try
40-
{
41-
zt.TransformDirectory(veryLong);
42-
Assert.Fail("Expected an exception");
43-
}
44-
catch (PathTooLongException)
45-
{
46-
}
60+
var tooLong = new string('x', 65536);
61+
Assert.Throws<PathTooLongException>(() => zt.TransformDirectory(tooLong));
4762
}
4863

4964
[Test]
5065
public void LengthBoundaryOk()
5166
{
5267
var zt = new ZipNameTransform();
53-
string veryLong = "c:\\" + new string('x', 65535);
54-
try
55-
{
56-
zt.TransformDirectory(veryLong);
57-
}
58-
catch
59-
{
60-
Assert.Fail("Expected no exception");
61-
}
68+
var tooLongWithRoot = Utils.SystemRoot + new string('x', 65535);
69+
Assert.DoesNotThrow(() => zt.TransformDirectory(tooLongWithRoot));
6270
}
6371

6472
[Test]
6573
[Category("Zip")]
66-
public void NameTransforms()
74+
[Platform("Win")]
75+
public void NameTransforms_Windows()
6776
{
6877
INameTransform t = new ZipNameTransform(@"C:\Slippery");
6978
Assert.AreEqual("Pongo/Directory/", t.TransformDirectory(@"C:\Slippery\Pongo\Directory"), "Value should be trimmed and converted");
7079
Assert.AreEqual("PoNgo/Directory/", t.TransformDirectory(@"c:\slipperY\PoNgo\Directory"), "Trimming should be case insensitive");
71-
Assert.AreEqual("slippery/Pongo/Directory/", t.TransformDirectory(@"d:\slippery\Pongo\Directory"), "Trimming should be case insensitive");
80+
Assert.AreEqual("slippery/Pongo/Directory/", t.TransformDirectory(@"d:\slippery\Pongo\Directory"), "Trimming should account for root");
7281

7382
Assert.AreEqual("Pongo/File", t.TransformFile(@"C:\Slippery\Pongo\File"), "Value should be trimmed and converted");
7483
}
84+
85+
[Test]
86+
[Category("Zip")]
87+
[Platform(Exclude="Win")]
88+
public void NameTransforms_Posix()
89+
{
90+
INameTransform t = new ZipNameTransform(@"/Slippery");
91+
Assert.AreEqual("Pongo/Directory/", t.TransformDirectory(@"/Slippery\Pongo\Directory"), "Value should be trimmed and converted");
92+
Assert.AreEqual("PoNgo/Directory/", t.TransformDirectory(@"/slipperY\PoNgo\Directory"), "Trimming should be case insensitive");
93+
Assert.AreEqual("slippery/Pongo/Directory/", t.TransformDirectory(@"/slippery/slippery/Pongo/Directory"), "Trimming should account for root");
94+
95+
Assert.AreEqual("Pongo/File", t.TransformFile(@"/Slippery/Pongo/File"), "Value should be trimmed and converted");
96+
}
7597

7698
/// <summary>
7799
/// Test ZipEntry static file name cleaning methods

0 commit comments

Comments
 (0)