Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify DropPathRoot and fix out of bounds issue #593

Merged
merged 4 commits into from Apr 27, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 15 additions & 43 deletions src/ICSharpCode.SharpZipLib/Core/PathUtils.cs
@@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Linq;

namespace ICSharpCode.SharpZipLib.Core
{
Expand All @@ -12,51 +14,21 @@ public static class PathUtils
/// </summary>
/// <param name="path">A <see cref="string"/> containing path information.</param>
/// <returns>The path with the root removed if it was present; path otherwise.</returns>
/// <remarks>Unlike the <see cref="System.IO.Path"/> class the path isn't otherwise checked for validity.</remarks>
public static string DropPathRoot(string path)
{
string result = path;

if (!string.IsNullOrEmpty(path))
{
if ((path[0] == '\\') || (path[0] == '/'))
{
// UNC name ?
if ((path.Length > 1) && ((path[1] == '\\') || (path[1] == '/')))
{
int index = 2;
int elements = 2;

// Scan for two separate elements \\machine\share\restofpath
while ((index <= path.Length) &&
(((path[index] != '\\') && (path[index] != '/')) || (--elements > 0)))
{
index++;
}

index++;

if (index < path.Length)
{
result = path.Substring(index);
}
else
{
result = "";
}
}
}
else if ((path.Length > 1) && (path[1] == ':'))
{
int dropCount = 2;
if ((path.Length > 2) && ((path[2] == '\\') || (path[2] == '/')))
{
dropCount = 3;
}
result = result.Remove(0, dropCount);
}
}
return result;
var invalidChars = Path.GetInvalidPathChars();
// If the first character after the root is a ':', .NET < 4.6.2 throws
var cleanRootSep = path.Length >= 3 && path[1] == ':' && path[2] == ':';

// Replace any invalid path characters with '_' to prevent Path.GetPathRoot from throwing.
// Only pass the first 258 (should be 260, but that still throws for some reason) characters
// as .NET < 4.6.2 throws on longer paths
var cleanPath = new string(path.Take(258)
.Select( (c, i) => invalidChars.Contains(c) || (i == 2 && cleanRootSep) ? '_' : c).ToArray());

var stripLength = Path.GetPathRoot(cleanPath).Length;
while (path.Length > stripLength && (path[stripLength] == '/' || path[stripLength] == '\\')) stripLength++;
return path.Substring(stripLength);
}

/// <summary>
Expand Down
47 changes: 47 additions & 0 deletions test/ICSharpCode.SharpZipLib.Tests/Core/CoreTests.cs
@@ -1,3 +1,4 @@
using System;
using ICSharpCode.SharpZipLib.Core;
using NUnit.Framework;

Expand Down Expand Up @@ -54,5 +55,51 @@ public void ValidFilter()
Assert.IsFalse(NameFilter.IsValidFilterExpression(@"\,)"));
Assert.IsFalse(NameFilter.IsValidFilterExpression(@"[]"));
}

// Use a shorter name wrapper to make tests more legible
private static string DropRoot(string s) => PathUtils.DropPathRoot(s);

[Test]
[Category("Core")]
[Platform("Win")]
public void DropPathRoot_Windows()
{
Assert.AreEqual("file.txt", DropRoot(@"\\server\share\file.txt"));
Assert.AreEqual("file.txt", DropRoot(@"c:\file.txt"));
Assert.AreEqual(@"subdir with spaces\file.txt", DropRoot(@"z:\subdir with spaces\file.txt"));
Assert.AreEqual("", DropRoot(@"\\server\share\"));
Assert.AreEqual(@"server\share\file.txt", DropRoot(@"\server\share\file.txt"));
Assert.AreEqual(@"path\file.txt", DropRoot(@"\\server\share\\path\file.txt"));
}

[Test]
[Category("Core")]
[Platform(Exclude="Win")]
public void DropPathRoot_Posix()
{
Assert.AreEqual("file.txt", DropRoot("/file.txt"));
Assert.AreEqual(@"tmp/file.txt", DropRoot(@"/tmp/file.txt"));
Assert.AreEqual(@"tmp\file.txt", DropRoot(@"\tmp\file.txt"));
Assert.AreEqual(@"tmp/file.txt", DropRoot(@"\tmp/file.txt"));
Assert.AreEqual(@"tmp\file.txt", DropRoot(@"/tmp\file.txt"));
Assert.AreEqual("", DropRoot("/"));

}

[Test]
[TestCase(@"c:\file:+/")]
[TestCase(@"c:\file*?")]
[TestCase("c:\\file|\"")]
[TestCase(@"c:\file<>")]
[TestCase(@"c:file")]
[TestCase(@"c::file")]
[TestCase(@"c:?file")]
[TestCase(@"c:+file")]
[TestCase(@"cc:file")]
[Category("Core")]
public void DropPathRoot_DoesNotThrowForInvalidPath(string path)
{
Assert.DoesNotThrow(() => Console.WriteLine(PathUtils.DropPathRoot(path)));
}
}
}
17 changes: 15 additions & 2 deletions test/ICSharpCode.SharpZipLib.Tests/TestSupport/SevenZip.cs
Expand Up @@ -17,7 +17,7 @@ internal static class SevenZipHelper
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "7-Zip", "7z.exe"),
};

public static bool TryGet7zBinPath(out string path7z)
private static bool TryGet7zBinPath(out string path7z)
{
var runTimeLimit = TimeSpan.FromSeconds(3);

Expand Down Expand Up @@ -77,12 +77,25 @@ internal static void VerifyZipWith7Zip(Stream zipStream, string password)
zipStream.CopyTo(fs);
}

var p = Process.Start(path7z, $"t -p{password} \"{fileName}\"");
var p = Process.Start(new ProcessStartInfo(path7z, $"t -p{password} \"{fileName}\"")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
});

if (p == null)
{
Assert.Inconclusive("Failed to start 7z process. Skipping!");
}
if (!p.WaitForExit(2000))
{
Assert.Warn("Timed out verifying zip file!");
}

TestContext.Out.Write(p.StandardOutput.ReadToEnd());
var errors = p.StandardError.ReadToEnd();
Assert.IsEmpty(errors, "7z reported errors");
Assert.AreEqual(0, p.ExitCode, "Archive verification failed");
}
finally
Expand Down
7 changes: 7 additions & 0 deletions test/ICSharpCode.SharpZipLib.Tests/TestSupport/Utils.cs
Expand Up @@ -13,9 +13,16 @@ public static class Utils
public static int DummyContentLength = 16;

private static Random random = new Random();

/// <summary>
/// Returns the system root for the current platform (usually c:\ for windows and / for others)
/// </summary>
public static string SystemRoot { get; } =
Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData));

private static void Compare(byte[] a, byte[] b)
{

if (a == null)
{
throw new ArgumentNullException(nameof(a));
Expand Down
9 changes: 7 additions & 2 deletions test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs
Expand Up @@ -23,6 +23,7 @@ public void NullStreamDetected()

try
{
// ReSharper disable once ExpressionIsAlwaysNull
bad = new ZipFile(nullStream);
}
catch
Expand Down Expand Up @@ -439,17 +440,21 @@ public void AddAndDeleteEntriesMemory()
f.IsStreamOwner = false;

f.BeginUpdate(new MemoryArchiveStorage());
f.Add(new StringMemoryDataSource("Hello world"), @"z:\a\a.dat");
f.Add(new StringMemoryDataSource("Hello world"), Utils.SystemRoot + @"a\a.dat");
f.Add(new StringMemoryDataSource("Another"), @"\b\b.dat");
f.Add(new StringMemoryDataSource("Mr C"), @"c\c.dat");
f.Add(new StringMemoryDataSource("Mrs D was a star"), @"d\d.dat");
f.CommitUpdate();
Assert.IsTrue(f.TestArchive(true));
foreach (ZipEntry entry in f)
{
Console.WriteLine($" - {entry.Name}");
}
}

byte[] master = memStream.ToArray();

TryDeleting(master, 4, 1, @"z:\a\a.dat");
TryDeleting(master, 4, 1, Utils.SystemRoot + @"a\a.dat");
TryDeleting(master, 4, 1, @"\a\a.dat");
TryDeleting(master, 4, 1, @"a/a.dat");

Expand Down
66 changes: 44 additions & 22 deletions test/ICSharpCode.SharpZipLib.Tests/Zip/ZipNameTransformHandling.cs
Expand Up @@ -3,6 +3,7 @@
using NUnit.Framework;
using System;
using System.IO;
using ICSharpCode.SharpZipLib.Tests.TestSupport;

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

TestFile(t, "abcdef", "abcdef");
TestFile(t, @"\\uncpath\d1\file1", "file1");
TestFile(t, @"C:\absolute\file2", "absolute/file2");

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

// Trick filenames.
TestFile(t, @".....file3", ".....file3");
}

[Test]
[Category("Zip")]
[Platform("Win")]
public void Basic_Windows()
{
var t = new ZipNameTransform();
TestFile(t, @"\\uncpath\d1\file1", "file1");
TestFile(t, @"C:\absolute\file2", "absolute/file2");

TestFile(t, @"c::file", "_file");
}

[Test]
[Category("Zip")]
[Platform(Exclude="Win")]
public void Basic_Posix()
{
var t = new ZipNameTransform();
TestFile(t, @"backslash_path\file1", "backslash_path/file1");
TestFile(t, "/absolute/file2", "absolute/file2");

TestFile(t, @"////////:file", "_file");
}

[Test]
public void TooLong()
{
var zt = new ZipNameTransform();
var veryLong = new string('x', 65536);
try
{
zt.TransformDirectory(veryLong);
Assert.Fail("Expected an exception");
}
catch (PathTooLongException)
{
}
var tooLong = new string('x', 65536);
Assert.Throws<PathTooLongException>(() => zt.TransformDirectory(tooLong));
}

[Test]
public void LengthBoundaryOk()
{
var zt = new ZipNameTransform();
string veryLong = "c:\\" + new string('x', 65535);
try
{
zt.TransformDirectory(veryLong);
}
catch
{
Assert.Fail("Expected no exception");
}
var tooLongWithRoot = Utils.SystemRoot + new string('x', 65535);
Assert.DoesNotThrow(() => zt.TransformDirectory(tooLongWithRoot));
}

[Test]
[Category("Zip")]
public void NameTransforms()
[Platform("Win")]
public void NameTransforms_Windows()
{
INameTransform t = new ZipNameTransform(@"C:\Slippery");
Assert.AreEqual("Pongo/Directory/", t.TransformDirectory(@"C:\Slippery\Pongo\Directory"), "Value should be trimmed and converted");
Assert.AreEqual("PoNgo/Directory/", t.TransformDirectory(@"c:\slipperY\PoNgo\Directory"), "Trimming should be case insensitive");
Assert.AreEqual("slippery/Pongo/Directory/", t.TransformDirectory(@"d:\slippery\Pongo\Directory"), "Trimming should be case insensitive");
Assert.AreEqual("slippery/Pongo/Directory/", t.TransformDirectory(@"d:\slippery\Pongo\Directory"), "Trimming should account for root");

Assert.AreEqual("Pongo/File", t.TransformFile(@"C:\Slippery\Pongo\File"), "Value should be trimmed and converted");
}

[Test]
[Category("Zip")]
[Platform(Exclude="Win")]
public void NameTransforms_Posix()
{
INameTransform t = new ZipNameTransform(@"/Slippery");
Assert.AreEqual("Pongo/Directory/", t.TransformDirectory(@"/Slippery\Pongo\Directory"), "Value should be trimmed and converted");
Assert.AreEqual("PoNgo/Directory/", t.TransformDirectory(@"/slipperY\PoNgo\Directory"), "Trimming should be case insensitive");
Assert.AreEqual("slippery/Pongo/Directory/", t.TransformDirectory(@"/slippery/slippery/Pongo/Directory"), "Trimming should account for root");

Assert.AreEqual("Pongo/File", t.TransformFile(@"/Slippery/Pongo/File"), "Value should be trimmed and converted");
}

/// <summary>
/// Test ZipEntry static file name cleaning methods
Expand Down