2021-05-31 18:21:39 +01:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.IO;
|
2021-06-02 14:41:06 +01:00
|
|
|
using System.Text;
|
2022-12-07 20:39:49 +00:00
|
|
|
using System.Text.Json;
|
|
|
|
|
using System.Text.Json.Serialization;
|
2021-05-31 18:21:39 +01:00
|
|
|
using Aaru.Checksums;
|
|
|
|
|
using Aaru.CommonTypes;
|
2021-09-16 04:42:14 +01:00
|
|
|
using Aaru.CommonTypes.Enums;
|
2021-05-31 18:21:39 +01:00
|
|
|
using Aaru.CommonTypes.Interfaces;
|
|
|
|
|
using Aaru.CommonTypes.Structs;
|
|
|
|
|
using Aaru.Core;
|
|
|
|
|
using FluentAssertions;
|
|
|
|
|
using NUnit.Framework;
|
|
|
|
|
using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes;
|
|
|
|
|
|
2022-11-15 15:58:43 +00:00
|
|
|
namespace Aaru.Tests.Filesystems;
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
public abstract class ReadOnlyFilesystemTest : FilesystemTest
|
|
|
|
|
{
|
2022-11-15 01:35:06 +00:00
|
|
|
protected ReadOnlyFilesystemTest() {}
|
2021-05-31 19:15:07 +01:00
|
|
|
|
2022-11-15 01:35:06 +00:00
|
|
|
protected ReadOnlyFilesystemTest(string fileSystemType) : base(fileSystemType) {}
|
2021-05-31 18:22:58 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
[Test]
|
|
|
|
|
public void Contents()
|
|
|
|
|
{
|
|
|
|
|
Environment.CurrentDirectory = DataFolder;
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Assert.Multiple(() =>
|
2021-05-31 20:37:52 +01:00
|
|
|
{
|
|
|
|
|
foreach(FileSystemTest test in Tests)
|
|
|
|
|
{
|
|
|
|
|
string testFile = test.TestFile;
|
2022-11-15 15:58:43 +00:00
|
|
|
bool found = false;
|
2021-05-31 20:37:52 +01:00
|
|
|
var partition = new Partition();
|
|
|
|
|
|
|
|
|
|
bool exists = File.Exists(testFile);
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.True(exists, string.Format(Localization._0_not_found, testFile));
|
2021-05-31 20:37:52 +01:00
|
|
|
|
|
|
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
|
|
|
|
// It arrives here...
|
|
|
|
|
if(!exists)
|
|
|
|
|
continue;
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
var filtersList = new FiltersList();
|
|
|
|
|
IFilter inputFilter = filtersList.GetFilter(testFile);
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.IsNotNull(inputFilter, string.Format(Localization.Filter_0, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
2022-03-07 07:36:44 +00:00
|
|
|
var image = ImageFormat.Detect(inputFilter) as IMediaImage;
|
2022-03-06 13:29:38 +00:00
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.IsNotNull(image, string.Format(Localization.Image_format_0, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.AreEqual(ErrorNumber.NoError, image.Open(inputFilter),
|
|
|
|
|
string.Format(Localization.Cannot_open_image_for_0, testFile));
|
2021-06-01 02:35:35 +01:00
|
|
|
|
2021-05-31 20:37:52 +01:00
|
|
|
List<string> idPlugins;
|
|
|
|
|
|
|
|
|
|
if(Partitions)
|
|
|
|
|
{
|
|
|
|
|
List<Partition> partitionsList = Core.Partitions.GetAll(image);
|
|
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.Greater(partitionsList.Count, 0,
|
|
|
|
|
string.Format(Localization.No_partitions_found_for_0, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
2021-05-31 20:37:52 +01:00
|
|
|
// In reverse to skip boot partitions we're not interested in
|
|
|
|
|
for(int index = partitionsList.Count - 1; index >= 0; index--)
|
|
|
|
|
{
|
2022-11-15 15:58:43 +00:00
|
|
|
Core.Filesystems.Identify(image, out idPlugins, partitionsList[index], true);
|
2021-05-31 20:37:52 +01:00
|
|
|
|
|
|
|
|
if(idPlugins.Count == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if(!idPlugins.Contains(Plugin.Id.ToString()))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
found = true;
|
|
|
|
|
partition = partitionsList[index];
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
partition = new Partition
|
|
|
|
|
{
|
|
|
|
|
Name = "Whole device",
|
|
|
|
|
Length = image.Info.Sectors,
|
|
|
|
|
Size = image.Info.Sectors * image.Info.SectorSize
|
|
|
|
|
};
|
|
|
|
|
|
2022-11-15 15:58:43 +00:00
|
|
|
Core.Filesystems.Identify(image, out idPlugins, partition, true);
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.Greater(idPlugins.Count, 0,
|
|
|
|
|
string.Format(Localization.No_filesystems_found_for_0, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
2021-05-31 20:37:52 +01:00
|
|
|
found = idPlugins.Contains(Plugin.Id.ToString());
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.True(found, string.Format(Localization.Filesystem_not_identified_for_0, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
2021-05-31 20:37:52 +01:00
|
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
|
|
|
|
// It is not the case, it changes
|
|
|
|
|
if(!found)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var fs = Activator.CreateInstance(Plugin.GetType()) as IReadOnlyFilesystem;
|
|
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.NotNull(fs, string.Format(Localization.Could_not_instantiate_filesystem_for_0, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
2021-06-02 14:41:06 +01:00
|
|
|
test.Encoding ??= Encoding.ASCII;
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
ErrorNumber ret = fs.Mount(image, partition, test.Encoding, null, test.Namespace);
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.AreEqual(ErrorNumber.NoError, ret, string.Format(Localization.Unmountable_0, testFile));
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-12-07 20:39:49 +00:00
|
|
|
var serializerOptions = new JsonSerializerOptions
|
2021-05-31 20:37:52 +01:00
|
|
|
{
|
2022-12-07 20:39:49 +00:00
|
|
|
Converters =
|
|
|
|
|
{
|
|
|
|
|
new JsonStringEnumConverter()
|
|
|
|
|
},
|
|
|
|
|
MaxDepth = 1536, // More than this an we get a StackOverflowException
|
|
|
|
|
WriteIndented = true,
|
|
|
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
|
|
|
PropertyNameCaseInsensitive = true
|
2021-06-01 03:58:26 +01:00
|
|
|
};
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(test.ContentsJson != null)
|
|
|
|
|
test.Contents =
|
2022-12-07 20:39:49 +00:00
|
|
|
JsonSerializer.Deserialize<Dictionary<string, FileData>>(test.ContentsJson, serializerOptions);
|
2022-03-06 13:29:38 +00:00
|
|
|
else if(File.Exists($"{testFile}.contents.json"))
|
|
|
|
|
{
|
2022-12-07 20:39:49 +00:00
|
|
|
var sr = new FileStream($"{testFile}.contents.json", FileMode.Open);
|
|
|
|
|
test.Contents = JsonSerializer.Deserialize<Dictionary<string, FileData>>(sr, serializerOptions);
|
2022-03-06 13:29:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(test.Contents is null)
|
|
|
|
|
continue;
|
|
|
|
|
|
2022-12-07 20:43:25 +00:00
|
|
|
int currentDepth = 0;
|
|
|
|
|
|
|
|
|
|
TestDirectory(fs, "/", test.Contents, testFile, true, out List<NextLevel> currentLevel, currentDepth);
|
|
|
|
|
|
|
|
|
|
while(currentLevel.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
currentDepth++;
|
|
|
|
|
List<NextLevel> nextLevels = new();
|
|
|
|
|
|
|
|
|
|
foreach(NextLevel subLevel in currentLevel)
|
|
|
|
|
{
|
|
|
|
|
TestDirectory(fs, subLevel.Path, subLevel.Children, testFile, true,
|
|
|
|
|
out List<NextLevel> nextLevel, currentDepth);
|
|
|
|
|
|
|
|
|
|
nextLevels.AddRange(nextLevel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentLevel = nextLevels;
|
|
|
|
|
}
|
2021-05-31 20:37:52 +01:00
|
|
|
}
|
2022-03-06 13:29:38 +00:00
|
|
|
});
|
|
|
|
|
}
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
[Test, Ignore("Not a test, do not run")]
|
|
|
|
|
public void Build()
|
|
|
|
|
{
|
|
|
|
|
Environment.CurrentDirectory = DataFolder;
|
|
|
|
|
|
|
|
|
|
foreach(FileSystemTest test in Tests)
|
2021-05-31 20:37:52 +01:00
|
|
|
{
|
2022-03-06 13:29:38 +00:00
|
|
|
string testFile = test.TestFile;
|
2022-11-15 15:58:43 +00:00
|
|
|
bool found = false;
|
2022-03-06 13:29:38 +00:00
|
|
|
var partition = new Partition();
|
|
|
|
|
|
|
|
|
|
bool exists = File.Exists(testFile);
|
|
|
|
|
|
|
|
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
|
|
|
|
// It arrives here...
|
|
|
|
|
if(!exists)
|
|
|
|
|
continue;
|
|
|
|
|
|
2022-11-15 15:58:43 +00:00
|
|
|
var filtersList = new FiltersList();
|
|
|
|
|
IFilter inputFilter = filtersList.GetFilter(testFile);
|
2022-03-17 00:46:26 +00:00
|
|
|
|
2022-11-14 01:49:10 +00:00
|
|
|
if(ImageFormat.Detect(inputFilter) is not IMediaImage image)
|
2022-03-17 00:46:26 +00:00
|
|
|
continue;
|
|
|
|
|
|
2022-11-15 15:58:43 +00:00
|
|
|
ErrorNumber opened = image.Open(inputFilter);
|
2021-06-02 14:41:27 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(opened != ErrorNumber.NoError)
|
|
|
|
|
continue;
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
List<string> idPlugins;
|
2021-09-12 19:27:43 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(Partitions)
|
2021-05-31 20:37:52 +01:00
|
|
|
{
|
2022-03-06 13:29:38 +00:00
|
|
|
List<Partition> partitionsList = Core.Partitions.GetAll(image);
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
// In reverse to skip boot partitions we're not interested in
|
|
|
|
|
for(int index = partitionsList.Count - 1; index >= 0; index--)
|
2021-05-31 20:37:52 +01:00
|
|
|
{
|
2022-11-15 15:58:43 +00:00
|
|
|
Core.Filesystems.Identify(image, out idPlugins, partitionsList[index], true);
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(idPlugins.Count == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if(!idPlugins.Contains(Plugin.Id.ToString()))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
found = true;
|
|
|
|
|
partition = partitionsList[index];
|
|
|
|
|
|
|
|
|
|
break;
|
2021-07-09 09:49:59 +01:00
|
|
|
}
|
2022-03-06 13:29:38 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
partition = new Partition
|
2021-05-31 20:37:52 +01:00
|
|
|
{
|
2022-03-06 13:29:38 +00:00
|
|
|
Name = "Whole device",
|
|
|
|
|
Length = image.Info.Sectors,
|
|
|
|
|
Size = image.Info.Sectors * image.Info.SectorSize
|
|
|
|
|
};
|
|
|
|
|
|
2022-11-15 15:58:43 +00:00
|
|
|
Core.Filesystems.Identify(image, out idPlugins, partition, true);
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
found = idPlugins.Contains(Plugin.Id.ToString());
|
2021-05-31 20:37:52 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
|
|
|
|
// It is not the case, it changes
|
|
|
|
|
if(!found)
|
|
|
|
|
continue;
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
var fs = Activator.CreateInstance(Plugin.GetType()) as IReadOnlyFilesystem;
|
|
|
|
|
|
|
|
|
|
test.Encoding ??= Encoding.ASCII;
|
|
|
|
|
|
|
|
|
|
fs?.Mount(image, partition, test.Encoding, null, test.Namespace);
|
|
|
|
|
|
2022-12-07 20:44:14 +00:00
|
|
|
Dictionary<string, FileData> contents = BuildDirectory(fs, "/", 0);
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-12-07 20:39:49 +00:00
|
|
|
var serializerOptions = new JsonSerializerOptions
|
2022-03-06 13:29:38 +00:00
|
|
|
{
|
2022-12-07 20:39:49 +00:00
|
|
|
Converters =
|
|
|
|
|
{
|
|
|
|
|
new JsonStringEnumConverter()
|
|
|
|
|
},
|
|
|
|
|
MaxDepth = 1536,
|
|
|
|
|
WriteIndented = true,
|
|
|
|
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
|
|
|
|
PropertyNameCaseInsensitive = true
|
2022-03-06 13:29:38 +00:00
|
|
|
};
|
|
|
|
|
|
2022-12-07 20:39:49 +00:00
|
|
|
var sw = new FileStream($"{testFile}.contents.json", FileMode.Create);
|
|
|
|
|
JsonSerializer.Serialize(sw, contents, serializerOptions);
|
2022-03-06 13:29:38 +00:00
|
|
|
sw.Close();
|
2021-05-31 20:37:52 +01:00
|
|
|
}
|
2022-03-06 13:29:38 +00:00
|
|
|
}
|
2021-05-31 20:37:52 +01:00
|
|
|
|
2022-12-09 13:30:51 +00:00
|
|
|
internal static Dictionary<string, FileData> BuildDirectory(IReadOnlyFilesystem fs, string path, int currentDepth)
|
2022-03-06 13:29:38 +00:00
|
|
|
{
|
2022-12-07 20:44:14 +00:00
|
|
|
currentDepth++;
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(path == "/")
|
|
|
|
|
path = "";
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Dictionary<string, FileData> children = new();
|
|
|
|
|
fs.ReadDir(path, out List<string> contents);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(contents is null)
|
|
|
|
|
return children;
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
foreach(string child in contents)
|
|
|
|
|
{
|
2022-11-15 15:58:43 +00:00
|
|
|
string childPath = $"{path}/{child}";
|
2022-03-06 13:29:38 +00:00
|
|
|
fs.Stat(childPath, out FileEntryInfo stat);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
var data = new FileData
|
|
|
|
|
{
|
|
|
|
|
Info = stat
|
|
|
|
|
};
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(stat.Attributes.HasFlag(FileAttributes.Directory))
|
2022-12-07 20:44:14 +00:00
|
|
|
{
|
|
|
|
|
// Cannot serialize to JSON too many depth levels 🤷♀️
|
|
|
|
|
if(currentDepth < 384)
|
|
|
|
|
data.Children = BuildDirectory(fs, childPath, currentDepth);
|
|
|
|
|
}
|
2022-03-06 13:29:38 +00:00
|
|
|
else if(stat.Attributes.HasFlag(FileAttributes.Symlink))
|
|
|
|
|
{
|
|
|
|
|
if(fs.ReadLink(childPath, out string link) == ErrorNumber.NoError)
|
|
|
|
|
data.LinkTarget = link;
|
|
|
|
|
}
|
|
|
|
|
else
|
2022-03-15 01:37:37 +00:00
|
|
|
data.Md5 = BuildFile(fs, childPath, stat.Length);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-12-08 14:16:35 +00:00
|
|
|
if(fs.ListXAttr(childPath, out List<string> xattrs) == ErrorNumber.NoError &&
|
|
|
|
|
xattrs.Count > 0)
|
|
|
|
|
data.XattrsWithMd5 = BuildFileXattrs(fs, childPath);
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
children[child] = data;
|
|
|
|
|
}
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
return children;
|
|
|
|
|
}
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
static string BuildFile(IReadOnlyFilesystem fs, string path, long length)
|
|
|
|
|
{
|
2022-11-15 15:58:43 +00:00
|
|
|
byte[] buffer = new byte[length];
|
2022-03-06 13:29:38 +00:00
|
|
|
fs.Read(path, 0, length, ref buffer);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
return Md5Context.Data(buffer, out _);
|
|
|
|
|
}
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-12-08 14:16:35 +00:00
|
|
|
static Dictionary<string, string> BuildFileXattrs(IReadOnlyFilesystem fs, string path)
|
|
|
|
|
{
|
|
|
|
|
fs.ListXAttr(path, out List<string> contents);
|
|
|
|
|
|
|
|
|
|
if(contents.Count == 0)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
Dictionary<string, string> xattrs = new();
|
|
|
|
|
|
|
|
|
|
foreach(string xattr in contents)
|
|
|
|
|
{
|
|
|
|
|
byte[] buffer = Array.Empty<byte>();
|
|
|
|
|
ErrorNumber ret = fs.GetXattr(path, xattr, ref buffer);
|
|
|
|
|
string data;
|
|
|
|
|
|
2022-12-11 15:50:50 +00:00
|
|
|
data = ret != ErrorNumber.NoError && ret != ErrorNumber.OutOfRange
|
|
|
|
|
? Md5Context.Data(Array.Empty<byte>(), out _) : Md5Context.Data(buffer, out _);
|
2022-12-08 14:16:35 +00:00
|
|
|
|
|
|
|
|
xattrs[xattr] = data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return xattrs;
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
internal static void TestDirectory(IReadOnlyFilesystem fs, string path, Dictionary<string, FileData> children,
|
2022-12-07 20:43:25 +00:00
|
|
|
string testFile, bool testXattr, out List<NextLevel> nextLevels,
|
|
|
|
|
int currentDepth)
|
2022-03-06 13:29:38 +00:00
|
|
|
{
|
2022-12-07 20:43:25 +00:00
|
|
|
currentDepth++;
|
|
|
|
|
nextLevels = new List<NextLevel>();
|
2022-03-06 13:29:38 +00:00
|
|
|
ErrorNumber ret = fs.ReadDir(path, out List<string> contents);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-12-07 20:43:25 +00:00
|
|
|
// Directory is not readable, probably filled the volume, just ignore it
|
|
|
|
|
if(ret == ErrorNumber.InvalidArgument)
|
|
|
|
|
return;
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Assert.AreEqual(ErrorNumber.NoError, ret,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Unexpected_error_0_when_reading_directory_1_of_2, ret, path,
|
|
|
|
|
testFile));
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-12-07 20:43:25 +00:00
|
|
|
if(ret != ErrorNumber.NoError)
|
|
|
|
|
return;
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(children.Count == 0 &&
|
|
|
|
|
contents.Count == 0)
|
|
|
|
|
return;
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(path == "/")
|
|
|
|
|
path = "";
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
List<string> expectedNotFound = new();
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
foreach(KeyValuePair<string, FileData> child in children)
|
|
|
|
|
{
|
2022-11-15 15:58:43 +00:00
|
|
|
string childPath = $"{path}/{child.Key}";
|
2022-03-06 13:29:38 +00:00
|
|
|
ret = fs.Stat(childPath, out FileEntryInfo stat);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(ret == ErrorNumber.NoSuchFile ||
|
|
|
|
|
contents is null ||
|
2022-11-15 15:58:43 +00:00
|
|
|
(ret == ErrorNumber.NoError && !contents.Contains(child.Key)))
|
2022-03-06 13:29:38 +00:00
|
|
|
{
|
|
|
|
|
expectedNotFound.Add(child.Key);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
contents.Remove(child.Key);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Assert.AreEqual(ErrorNumber.NoError, ret,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Unexpected_error_0_retrieving_stats_for_1_in_2, ret, childPath,
|
|
|
|
|
testFile));
|
2021-07-07 04:14:10 +01:00
|
|
|
|
2022-12-11 12:26:28 +00:00
|
|
|
if(child.Value.Info is not null)
|
|
|
|
|
{
|
|
|
|
|
if((stat.AccessTime - child.Value.Info.AccessTime)?.Hours is 1 or -1)
|
|
|
|
|
stat.AccessTime = child.Value.Info.AccessTime;
|
|
|
|
|
|
|
|
|
|
if((stat.AccessTimeUtc - child.Value.Info.AccessTimeUtc)?.Hours is 1 or -1)
|
|
|
|
|
stat.AccessTimeUtc = child.Value.Info.AccessTimeUtc;
|
|
|
|
|
|
|
|
|
|
if((stat.BackupTime - child.Value.Info.BackupTime)?.Hours is 1 or -1)
|
|
|
|
|
stat.BackupTime = child.Value.Info.BackupTime;
|
|
|
|
|
|
|
|
|
|
if((stat.BackupTimeUtc - child.Value.Info.BackupTimeUtc)?.Hours is 1 or -1)
|
|
|
|
|
stat.BackupTimeUtc = child.Value.Info.BackupTimeUtc;
|
|
|
|
|
|
|
|
|
|
if((stat.CreationTime - child.Value.Info.CreationTime)?.Hours is 1 or -1)
|
|
|
|
|
stat.CreationTime = child.Value.Info.CreationTime;
|
|
|
|
|
|
|
|
|
|
if((stat.CreationTimeUtc - child.Value.Info.CreationTimeUtc)?.Hours is 1 or -1)
|
|
|
|
|
stat.CreationTimeUtc = child.Value.Info.CreationTimeUtc;
|
|
|
|
|
|
|
|
|
|
if((stat.LastWriteTime - child.Value.Info.LastWriteTime)?.Hours is 1 or -1)
|
|
|
|
|
stat.LastWriteTime = child.Value.Info.LastWriteTime;
|
|
|
|
|
|
|
|
|
|
if((stat.LastWriteTimeUtc - child.Value.Info.LastWriteTimeUtc)?.Hours is 1 or -1)
|
|
|
|
|
stat.LastWriteTimeUtc = child.Value.Info.LastWriteTimeUtc;
|
|
|
|
|
|
|
|
|
|
if((stat.StatusChangeTime - child.Value.Info.StatusChangeTime)?.Hours is 1 or -1)
|
|
|
|
|
stat.StatusChangeTime = child.Value.Info.StatusChangeTime;
|
|
|
|
|
|
|
|
|
|
if((stat.StatusChangeTimeUtc - child.Value.Info.StatusChangeTimeUtc)?.Hours is 1 or -1)
|
|
|
|
|
stat.StatusChangeTimeUtc = child.Value.Info.StatusChangeTimeUtc;
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
stat.Should().BeEquivalentTo(child.Value.Info,
|
|
|
|
|
string.Format(Localization.Wrong_info_for_0_in_1, childPath, testFile));
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
byte[] buffer = Array.Empty<byte>();
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(child.Value.Info.Attributes.HasFlag(FileAttributes.Directory))
|
|
|
|
|
{
|
|
|
|
|
ret = fs.Read(childPath, 0, 1, ref buffer);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Assert.AreEqual(ErrorNumber.IsDirectory, ret,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Got_wrong_data_for_directory_0_in_1, childPath, testFile));
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-12-07 20:43:25 +00:00
|
|
|
// Cannot serialize to JSON too many depth levels 🤷♀️
|
|
|
|
|
if(currentDepth < 384)
|
|
|
|
|
{
|
|
|
|
|
Assert.IsNotNull(child.Value.Children,
|
|
|
|
|
string.
|
|
|
|
|
Format(Localization.Contents_for_0_in_1_must_be_defined_in_unit_test_declaration,
|
|
|
|
|
childPath, testFile));
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-12-07 20:43:25 +00:00
|
|
|
if(child.Value.Children != null)
|
|
|
|
|
{
|
|
|
|
|
nextLevels.Add(new NextLevel(childPath, child.Value.Children));
|
|
|
|
|
|
|
|
|
|
// TestDirectory(fs, childPath, child.Value.Children, testFile, testXattr);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-05-31 18:21:39 +01:00
|
|
|
}
|
2022-03-06 13:29:38 +00:00
|
|
|
else if(child.Value.Info.Attributes.HasFlag(FileAttributes.Symlink))
|
|
|
|
|
{
|
|
|
|
|
ret = fs.ReadLink(childPath, out string link);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Assert.AreEqual(ErrorNumber.NoError, ret,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Got_wrong_data_for_symbolic_link_0_in_1, childPath,
|
|
|
|
|
testFile));
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Assert.AreEqual(child.Value.LinkTarget, link,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Invalid_target_for_symbolic_link_0_in_1, childPath,
|
|
|
|
|
testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
}
|
|
|
|
|
else
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
// This ensure the buffer does not hang for collection
|
2022-03-15 01:37:37 +00:00
|
|
|
TestFile(fs, childPath, child.Value.Md5, child.Value.Info.Length, testFile);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(!testXattr)
|
|
|
|
|
continue;
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
ret = fs.ListXAttr(childPath, out List<string> xattrs);
|
|
|
|
|
|
|
|
|
|
if(ret == ErrorNumber.NotSupported)
|
|
|
|
|
{
|
|
|
|
|
Assert.IsNull(child.Value.XattrsWithMd5,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.
|
|
|
|
|
Format(Localization.Defined_extended_attributes_for_0_in_1_are_not_supported_by_filesystem,
|
|
|
|
|
childPath, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(ErrorNumber.NoError, ret,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Unexpected_error_0_when_listing_extended_attributes_for_1_in_2,
|
|
|
|
|
ret, childPath, testFile));
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(xattrs.Count > 0)
|
|
|
|
|
Assert.IsNotNull(child.Value.XattrsWithMd5,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.
|
|
|
|
|
Format(Localization.Extended_attributes_for_0_in_1_must_be_defined_in_unit_test_declaration,
|
|
|
|
|
childPath, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
|
|
|
|
if(xattrs.Count > 0 ||
|
|
|
|
|
child.Value.XattrsWithMd5?.Count > 0)
|
|
|
|
|
TestFileXattrs(fs, childPath, child.Value.XattrsWithMd5, testFile);
|
2021-05-31 18:21:39 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Assert.IsEmpty(expectedNotFound,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Could_not_find_the_children_of_0_in_1_2, path, testFile,
|
|
|
|
|
string.Join(" ", expectedNotFound)));
|
2021-09-19 21:16:47 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(contents != null)
|
|
|
|
|
Assert.IsEmpty(contents,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Found_the_following_unexpected_children_of_0_in_1_2, path,
|
|
|
|
|
testFile, string.Join(" ", contents)));
|
2022-03-06 13:29:38 +00:00
|
|
|
}
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
static void TestFile(IReadOnlyFilesystem fs, string path, string md5, long length, string testFile)
|
|
|
|
|
{
|
2022-11-15 15:58:43 +00:00
|
|
|
byte[] buffer = new byte[length];
|
2022-03-06 13:29:38 +00:00
|
|
|
ErrorNumber ret = fs.Read(path, 0, length, ref buffer);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-11-29 10:33:40 +00:00
|
|
|
Assert.AreEqual(ErrorNumber.NoError, ret,
|
|
|
|
|
string.Format(Localization.Unexpected_error_0_when_reading_1_in_2, ret, path, testFile));
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
string data = Md5Context.Data(buffer, out _);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
Assert.AreEqual(md5, data, $"Got MD5 {data} for \"{path}\" in {testFile} but expected {md5}");
|
|
|
|
|
}
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-07 07:36:44 +00:00
|
|
|
static void TestFileXattrs(IReadOnlyFilesystem fs, string path, Dictionary<string, string> xattrs, string testFile)
|
2022-03-06 13:29:38 +00:00
|
|
|
{
|
|
|
|
|
// Nothing to test
|
|
|
|
|
if(xattrs is null)
|
|
|
|
|
return;
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
fs.ListXAttr(path, out List<string> contents);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
if(xattrs.Count == 0 &&
|
|
|
|
|
contents.Count == 0)
|
|
|
|
|
return;
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
List<string> expectedNotFound = new();
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
foreach(KeyValuePair<string, string> xattr in xattrs)
|
|
|
|
|
{
|
|
|
|
|
byte[] buffer = Array.Empty<byte>();
|
|
|
|
|
ErrorNumber ret = fs.GetXattr(path, xattr.Key, ref buffer);
|
|
|
|
|
|
|
|
|
|
if(ret == ErrorNumber.NoSuchExtendedAttribute ||
|
|
|
|
|
!contents.Contains(xattr.Key))
|
|
|
|
|
{
|
|
|
|
|
expectedNotFound.Add(xattr.Key);
|
|
|
|
|
|
|
|
|
|
continue;
|
2021-05-31 18:21:39 +01:00
|
|
|
}
|
|
|
|
|
|
2022-03-06 13:29:38 +00:00
|
|
|
contents.Remove(xattr.Key);
|
2021-05-31 18:21:39 +01:00
|
|
|
|
2022-12-11 15:50:50 +00:00
|
|
|
// Partially read extended attribute... dunno why it happens with some Toast images
|
|
|
|
|
if(ret != ErrorNumber.OutOfRange)
|
|
|
|
|
Assert.AreEqual(ErrorNumber.NoError, ret,
|
|
|
|
|
string.Format(Localization.Unexpected_error_0_retrieving_extended_attributes_for_1_in_2,
|
|
|
|
|
ret, path, testFile));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
|
|
|
|
string data = Md5Context.Data(buffer, out _);
|
|
|
|
|
|
|
|
|
|
Assert.AreEqual(xattr.Value, data,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Got_MD5_0_for_1_of_2_in_3_but_expected_4, data, xattr.Key, path,
|
|
|
|
|
testFile, xattr.Value));
|
2021-05-31 18:21:39 +01:00
|
|
|
}
|
2022-03-06 13:29:38 +00:00
|
|
|
|
|
|
|
|
Assert.IsEmpty(expectedNotFound,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Could_not_find_the_following_extended_attributes_of_0_in_1_2, path,
|
|
|
|
|
testFile, string.Join(" ", expectedNotFound)));
|
2022-03-06 13:29:38 +00:00
|
|
|
|
|
|
|
|
Assert.IsEmpty(contents,
|
2022-11-29 10:33:40 +00:00
|
|
|
string.Format(Localization.Found_the_following_unexpected_extended_attributes_of_0_in_1_2, path,
|
|
|
|
|
testFile, string.Join(" ", contents)));
|
2021-05-31 18:21:39 +01:00
|
|
|
}
|
2022-12-07 20:43:25 +00:00
|
|
|
|
|
|
|
|
internal sealed record NextLevel(string Path, Dictionary<string, FileData> Children);
|
2021-05-31 18:21:39 +01:00
|
|
|
}
|