mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-11-24 22:06:59 +01:00
86fa2f6abf
* CA-375900: Prepend `//?/` to file paths when creating streams for archive generation The string works to enable creation of files with paths larger than 260 characters. * CA-375900: Add directory support and rename utility method * Fix whitespace in `ArchiveWriterTest` * CA-375900: Explicitly enumerate files and directories in `ArchiveWriter` `Directory.GetFiles` and `Directory.GetDirectories` do not enumerate if paths are longer than 260, even when prepended with `//?/`. * CA-375900: Add long path tests to `ArchiveWriter` * CA-375900: Add long path tests to `ArchiveIterator` * CA-375900: Ensure files are added to folders in archive * Use a recursive method to add directories and files to archive in `ArchiveIterator` Also improves progress reporting by basing it on directory count * Fix typos * Expand `ArchiveWriterTests` to cover all combinations of directory and path lengths * Ensure that directories used in recursive `Directory.Delete` calls are using long path format If files in the directory exceed the 260 character limit, the calls will fail * Expand `ArchiveIteratorTests` to cover all combinations of directory and path lengths * Ensure relative path name removes `rootPath` * Fix typo * Do not use long paths when importing OVFs The import uses `DiscUtils` which cannot handle paths prepended with `//?/` * Remove use of `ToLongWindowsPath` within appliance export This partially reverts commit 819425855c56c14b937849714b359003465bd2f4. * Refactoring and some corrections. Signed-off-by: Danilo Del Busso <danilo.delbusso@cloud.com> Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com> Co-authored-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>
299 lines
12 KiB
C#
299 lines
12 KiB
C#
/* Copyright (c) Cloud Software Group, Inc.
|
|
*
|
|
* Redistribution and use in source and binary forms,
|
|
* with or without modification, are permitted provided
|
|
* that the following conditions are met:
|
|
*
|
|
* * Redistributions of source code must retain the above
|
|
* copyright notice, this list of conditions and the
|
|
* following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the
|
|
* following disclaimer in the documentation and/or other
|
|
* materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using NUnit.Framework;
|
|
using XenCenterLib;
|
|
using XenCenterLib.Archive;
|
|
|
|
namespace XenAdminTests.ArchiveTests
|
|
{
|
|
[TestFixture, Category(TestCategories.Unit)]
|
|
public class ArchiveWriterTests
|
|
{
|
|
private class FakeArchiveWriter : ArchiveWriter
|
|
{
|
|
private bool disposed;
|
|
|
|
public List<Stream> AddedStreamData { get; private set; }
|
|
|
|
public List<string> AddedFileNameData { get; private set; }
|
|
|
|
public List<DateTime> AddedDates { get; private set; }
|
|
|
|
public FakeArchiveWriter()
|
|
{
|
|
Reset();
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
DisposeStreamList();
|
|
AddedStreamData = new List<Stream>();
|
|
AddedFileNameData = new List<string>();
|
|
AddedDates = new List<DateTime>();
|
|
}
|
|
|
|
private void DisposeStreamList()
|
|
{
|
|
if (AddedStreamData != null)
|
|
{
|
|
foreach (Stream stream in AddedStreamData)
|
|
{
|
|
if (stream != null)
|
|
stream.Dispose();
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Add(Stream fileToAdd, string fileName, DateTime modificationTime, Action cancellingDelegate)
|
|
{
|
|
disposed = false;
|
|
AddedStreamData.Add(fileToAdd);
|
|
AddedFileNameData.Add(fileName);
|
|
AddedDates.Add(modificationTime);
|
|
}
|
|
|
|
public override void AddDirectory(string directoryName, DateTime modificationTime)
|
|
{
|
|
AddedFileNameData.Add(directoryName);
|
|
AddedDates.Add(modificationTime);
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
if (disposing)
|
|
{
|
|
if (!disposed)
|
|
{
|
|
DisposeStreamList();
|
|
}
|
|
disposed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private FakeArchiveWriter fakeWriter;
|
|
|
|
[OneTimeSetUp]
|
|
public void FixtureSetup()
|
|
{
|
|
fakeWriter = new FakeArchiveWriter();
|
|
}
|
|
|
|
[OneTimeTearDown]
|
|
public void FixtureTearDown()
|
|
{
|
|
fakeWriter.Dispose();
|
|
}
|
|
|
|
[SetUp]
|
|
public void TestSetup()
|
|
{
|
|
fakeWriter.Reset();
|
|
}
|
|
|
|
[Test]
|
|
public void DatelessAddCallsImplementation()
|
|
{
|
|
const string fileName = "test.file";
|
|
using (MemoryStream ms = new MemoryStream(Encoding.ASCII.GetBytes("This is a test")))
|
|
{
|
|
fakeWriter.Add(ms, fileName, DateTime.Now, null);
|
|
Assert.AreEqual(1, fakeWriter.AddedFileNameData.Count);
|
|
Assert.AreEqual(1, fakeWriter.AddedStreamData.Count);
|
|
Assert.AreEqual(1, fakeWriter.AddedDates.Count);
|
|
Assert.AreEqual(fileName, fakeWriter.AddedFileNameData[0], "File name");
|
|
Assert.IsTrue(fakeWriter.AddedStreamData[0].Length == 14, "Stream has data");
|
|
Assert.That(fakeWriter.AddedDates[0], Is.EqualTo(DateTime.Now).Within(TimeSpan.FromSeconds(5)));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void DatelessAddDirectoryCallsImplementation()
|
|
{
|
|
const string dirName = "test.file";
|
|
const int totalAdded = 3;
|
|
for (int i = 0; i < totalAdded; i++)
|
|
{
|
|
fakeWriter.AddDirectory(dirName, DateTime.Now);
|
|
}
|
|
|
|
Assert.AreEqual(totalAdded, fakeWriter.AddedFileNameData.Count);
|
|
Assert.AreEqual(0, fakeWriter.AddedStreamData.Count);
|
|
Assert.AreEqual(totalAdded, fakeWriter.AddedDates.Count);
|
|
Assert.AreEqual(dirName, fakeWriter.AddedFileNameData[0], "File name");
|
|
Assert.That(fakeWriter.AddedDates[0], Is.EqualTo(DateTime.Now).Within(TimeSpan.FromSeconds(5)));
|
|
}
|
|
|
|
[Test]
|
|
public void CreateArchiveThrowsWithBadPath()
|
|
{
|
|
Assert.Throws(typeof(FileNotFoundException), () => fakeWriter.CreateArchive("Yellow brick road - not a path!"));
|
|
}
|
|
|
|
[TestCase(true, true)]
|
|
[TestCase(false, true)]
|
|
[TestCase(true, false)]
|
|
[TestCase(false, false)]
|
|
public void CreateArchiveWithLongPath(bool longDirectoryPath, bool longFilePath)
|
|
{
|
|
//set up the path to zip
|
|
var zipPath = PopulateLongPathArchive(true, longDirectoryPath, longFilePath, out var addedData);
|
|
|
|
fakeWriter.CreateArchive(zipPath);
|
|
|
|
foreach (var datum in addedData)
|
|
Assert.Contains(datum, fakeWriter.AddedFileNameData);
|
|
|
|
// 2 folders and one file
|
|
Assert.AreEqual(addedData.Count, fakeWriter.AddedFileNameData.Count);
|
|
|
|
//clean up: we need to ensure we're deleting the folder
|
|
if (longDirectoryPath || longFilePath)
|
|
zipPath = StringUtility.ToLongWindowsPathUnchecked(zipPath);
|
|
|
|
Directory.Delete(zipPath, true);
|
|
}
|
|
|
|
[Test]
|
|
public void CreateArchiveWithLongPath_PathTooLong()
|
|
{
|
|
//! N.B.: If this test fails it might be because the project has moved to a version of .NET Core
|
|
//! that does not require calls to `StringUtils.ToLongWindowsPath`. Please review its uses
|
|
//! and remove it from the codebase if possible.
|
|
|
|
// this test ensures PopulateLongPathArchive's correctness
|
|
// since CreateArchiveWithLongPath depends on it
|
|
|
|
Assert.DoesNotThrow(() => PopulateLongPathArchive(false, false, false, out _));
|
|
Assert.Throws<DirectoryNotFoundException>(() => PopulateLongPathArchive(false, false, true, out _));
|
|
Assert.Throws<PathTooLongException>(() => PopulateLongPathArchive(false, true, true, out _));
|
|
Assert.Throws<PathTooLongException>(() => PopulateLongPathArchive(false, true, false, out _));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set up method creating a directory containing 2 subdirectories one of which has a file
|
|
/// </summary>
|
|
/// <param name="createValidPaths">set to true to ensure folders and files are prepended with \\?\</param>
|
|
/// <returns>the path to the folder</returns>
|
|
private string PopulateLongPathArchive(bool createValidPaths, bool longDirectoryPath, bool longFilePath, out List<string> addedData)
|
|
{
|
|
var zipPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
|
Directory.CreateDirectory(zipPath);
|
|
|
|
var dirCharNumber1 = (longDirectoryPath ? 248 : 247) - zipPath.Length - 2;
|
|
//2 was removed for the combining slash between tempPath and dir, and the first character
|
|
var relativeDirectoryPath1 = 0 + new string('A', dirCharNumber1);
|
|
|
|
var dirCharNumber2 = (longDirectoryPath ? 248 : 247) - zipPath.Length - 3;
|
|
//3 was removed for the combining slash between zipPath and dir, the first character,
|
|
//and the combining slash between dir and filename
|
|
var relativeDirectoryPath2 = 1 + new string('A', dirCharNumber2);
|
|
|
|
var fileCharNumber = (longFilePath ? 260 : 259) - Path.Combine(zipPath, relativeDirectoryPath2).Length - 1;
|
|
//1 was removed for the combining slash between the full dir path and filename
|
|
var fileName = new string('B', fileCharNumber);
|
|
var relativeFilePath = Path.Combine(relativeDirectoryPath2, fileName);
|
|
|
|
addedData = new List<string>
|
|
{
|
|
relativeDirectoryPath1.Replace(@"\", "/"),
|
|
relativeDirectoryPath2.Replace(@"\", "/"),
|
|
relativeFilePath.Replace(@"\", "/")
|
|
};
|
|
|
|
var directoryPath1 = Path.Combine(zipPath, relativeDirectoryPath1);
|
|
var directoryPath2 = Path.Combine(zipPath, relativeDirectoryPath2);
|
|
var filePath = Path.Combine(directoryPath2, fileName);
|
|
|
|
if (createValidPaths)
|
|
{
|
|
directoryPath1 = StringUtility.ToLongWindowsPathUnchecked(directoryPath1);
|
|
directoryPath2 = StringUtility.ToLongWindowsPathUnchecked(directoryPath2);
|
|
filePath = StringUtility.ToLongWindowsPathUnchecked(filePath);
|
|
}
|
|
|
|
Directory.CreateDirectory(directoryPath1);
|
|
Directory.CreateDirectory(directoryPath2);
|
|
File.WriteAllText(filePath, "Hello, World!");
|
|
|
|
|
|
return zipPath;
|
|
}
|
|
|
|
[Test]
|
|
public void CreateArchiveWorksWithValidDirectoryStructure()
|
|
{
|
|
string tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
|
Directory.CreateDirectory(tempPath);
|
|
CreateFiles(tempPath, 2);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
string subfolder = Path.Combine(tempPath, Path.GetRandomFileName());
|
|
Directory.CreateDirectory(subfolder);
|
|
CreateFiles(subfolder, i);
|
|
}
|
|
|
|
fakeWriter.CreateArchive(tempPath);
|
|
|
|
Assert.AreEqual(12, fakeWriter.AddedDates.Count);
|
|
Assert.AreEqual(12, fakeWriter.AddedFileNameData.Count);
|
|
Assert.AreEqual(8, fakeWriter.AddedStreamData.Count);
|
|
|
|
foreach (DateTime date in fakeWriter.AddedDates)
|
|
Assert.That(date, Is.EqualTo(DateTime.Now).Within(TimeSpan.FromSeconds(5)));
|
|
|
|
foreach (string name in fakeWriter.AddedFileNameData)
|
|
Assert.AreEqual(-1, name.IndexOfAny(@":\".ToArray()), "Unwanted chars found in path");
|
|
|
|
Directory.Delete(tempPath, true);
|
|
}
|
|
|
|
private void CreateFiles(string tempPath, int numberOfFiles)
|
|
{
|
|
for (int i = 0; i < numberOfFiles; i++)
|
|
{
|
|
using (FileStream fs = File.OpenWrite(Path.Combine(tempPath, Path.GetRandomFileName())))
|
|
{
|
|
fs.Write(Encoding.ASCII.GetBytes("This is a test"), 0, 14);
|
|
fs.Flush();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|