diff --git a/DiscUtils.sln.DotSettings b/DiscUtils.sln.DotSettings
index d0ab9c465..71ad46345 100644
--- a/DiscUtils.sln.DotSettings
+++ b/DiscUtils.sln.DotSettings
@@ -219,9 +219,11 @@
True
True
True
+ True
True
True
True
True
True
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/Library/DiscUtils.Fat/Directory.cs b/Library/DiscUtils.Fat/Directory.cs
index 04683f29e..3feb3f4c7 100644
--- a/Library/DiscUtils.Fat/Directory.cs
+++ b/Library/DiscUtils.Fat/Directory.cs
@@ -35,14 +35,23 @@ internal class Directory : IDisposable
private readonly long _parentId;
private long _endOfEntries;
- private Dictionary _entries;
- private List _freeEntries;
+ private readonly Dictionary _entries;
+ private readonly Dictionary _shortFileNameToEntry;
+ private readonly Dictionary _fullFileNameToEntry;
+
+ private FreeDirectoryEntryTable _freeDirectoryEntryTable;
+
private DirectoryEntry _parentEntry;
private long _parentEntryLocation;
private DirectoryEntry _selfEntry;
private long _selfEntryLocation;
+ ///
+ /// Delegate to check if a short name exists in the directory used by
+ ///
+ internal readonly Func CheckIfShortNameExists;
+
///
/// Initializes a new instance of the Directory class. Use this constructor to represent non-root directories.
///
@@ -51,8 +60,13 @@ internal class Directory : IDisposable
internal Directory(Directory parent, long parentId)
{
FileSystem = parent.FileSystem;
+ _entries = new();
+ _freeDirectoryEntryTable = new();
+ _shortFileNameToEntry = new(StringComparer.OrdinalIgnoreCase);
+ _fullFileNameToEntry = new(StringComparer.OrdinalIgnoreCase);
_parent = parent;
_parentId = parentId;
+ CheckIfShortNameExists = CheckIfShortNameExistsImpl;
var dirEntry = ParentsChildEntry;
_dirStream = new ClusterStream(FileSystem, FileAccess.ReadWrite, dirEntry.FirstCluster, uint.MaxValue);
@@ -68,7 +82,12 @@ internal Directory(Directory parent, long parentId)
internal Directory(FatFileSystem fileSystem, Stream dirStream)
{
FileSystem = fileSystem;
+ _entries = new();
+ _freeDirectoryEntryTable = new();
+ _shortFileNameToEntry = new(StringComparer.OrdinalIgnoreCase);
+ _fullFileNameToEntry = new(StringComparer.OrdinalIgnoreCase);
_dirStream = dirStream;
+ CheckIfShortNameExists = CheckIfShortNameExistsImpl;
LoadEntries();
}
@@ -112,7 +131,7 @@ public DirectoryEntry GetEntry(long id)
return id < 0 ? null : _entries[id];
}
- public Directory GetChildDirectory(FileName name)
+ public Directory GetChildDirectory(string name)
{
var id = FindEntry(name);
if (id < 0)
@@ -128,7 +147,7 @@ public Directory GetChildDirectory(FileName name)
return FileSystem.GetDirectory(this, id);
}
- internal Directory CreateChildDirectory(FileName name)
+ internal Directory CreateChildDirectory(string name)
{
var id = FindEntry(name);
if (id >= 0)
@@ -150,7 +169,17 @@ internal Directory CreateChildDirectory(FileName name)
FileSystem.Fat.SetEndOfChain(firstCluster);
- var newEntry = new DirectoryEntry(FileSystem.FatOptions, name, FatAttributes.Directory,
+ FatFileName fatFileName;
+ try
+ {
+ fatFileName = FatFileName.FromName(name, FileSystem.FatOptions.FileNameEncodingTable, CheckIfShortNameExists);
+ }
+ catch (ArgumentException ex)
+ {
+ throw new IOException($"Invalid directory name {name}", ex);
+ }
+
+ var newEntry = new DirectoryEntry(FileSystem.FatOptions, fatFileName, FatAttributes.Directory,
FileSystem.FatVariant)
{
FirstCluster = firstCluster,
@@ -172,7 +201,7 @@ internal Directory CreateChildDirectory(FileName name)
}
}
- internal void AttachChildDirectory(FileName name, Directory newChild)
+ internal void AttachChildDirectory(string name, Directory newChild)
{
var id = FindEntry(name);
if (id >= 0)
@@ -180,16 +209,20 @@ internal void AttachChildDirectory(FileName name, Directory newChild)
throw new IOException("Directory entry already exists");
}
- var newEntry = new DirectoryEntry(newChild.ParentsChildEntry)
+ FatFileName fatFileName;
+ try
{
- Name = name
- };
+ fatFileName = FatFileName.FromName(name, FileSystem.FatOptions.FileNameEncodingTable, CheckIfShortNameExists);
+ }
+ catch (ArgumentException ex)
+ {
+ throw new IOException($"Invalid directory name {name}", ex);
+ }
+
+ var newEntry = new DirectoryEntry(newChild.ParentsChildEntry, fatFileName);
AddEntry(newEntry);
- var newParentEntry = new DirectoryEntry(SelfEntry)
- {
- Name = FileName.ParentEntryName
- };
+ var newParentEntry = new DirectoryEntry(SelfEntry, FatFileName.ParentEntryName);
newChild.ParentEntry = newParentEntry;
}
@@ -207,27 +240,50 @@ internal long FindVolumeId()
return -1;
}
- internal long FindEntry(FileName name)
+ internal long FindEntry(string name)
{
- foreach (var id in _entries.Keys)
+#if NET6_0_OR_GREATER
+ return _fullFileNameToEntry.GetValueOrDefault(name, -1);
+#else
+ if (_fullFileNameToEntry.TryGetValue(name, out var id))
{
- var focus = _entries[id];
- if (focus.Name == name && (focus.Attributes & FatAttributes.VolumeId) == 0)
- {
- return id;
- }
+ return id;
}
return -1;
+#endif
}
- internal FatFileStream OpenFile(FileName name, FileMode mode, FileAccess fileAccess)
+ public void ReplaceShortName(long id, string name)
{
- if (mode is FileMode.Append or FileMode.Truncate)
+ var entry = _entries[id];
+
+ if (entry.Name.ShortName.Equals(name, StringComparison.OrdinalIgnoreCase))
{
- throw new NotImplementedException();
+ return;
}
+ if (_shortFileNameToEntry.ContainsKey(name))
+ {
+ throw new IOException("Short name already exists in directory");
+ }
+
+ // Save the current name so we can update the lookup tables if the rename is successful
+ var previousName = entry.Name;
+
+ // This might fail so we can only update the dictionary lookup tables if it succeeds
+ entry.ReplaceShortName(name, FileSystem.FatOptions.FileNameEncodingTable);
+
+ _shortFileNameToEntry.Remove(previousName.ShortName);
+ _fullFileNameToEntry.Remove(previousName.FullName);
+ _shortFileNameToEntry.Add(entry.Name.ShortName, id);
+ _fullFileNameToEntry.Add(entry.Name.FullName, id);
+ }
+
+ private bool CheckIfShortNameExistsImpl(string shortName) => _shortFileNameToEntry.ContainsKey(shortName);
+
+ internal FatFileStream OpenFile(string name, FileMode mode, FileAccess fileAccess)
+ {
var fileId = FindEntry(name);
var exists = fileId != -1;
@@ -236,19 +292,22 @@ internal FatFileStream OpenFile(FileName name, FileMode mode, FileAccess fileAcc
throw new IOException("File already exists");
}
- if (mode == FileMode.Open && !exists)
+ if ((mode == FileMode.Open || mode == FileMode.Truncate || mode == FileMode.Append) && !exists)
{
- throw new FileNotFoundException("File not found",
- name.GetDisplayName(FileSystem.FatOptions.FileNameEncoding));
+ throw new FileNotFoundException("File not found", name);
}
- if ((mode == FileMode.Open || mode == FileMode.OpenOrCreate || mode == FileMode.Create) && exists)
+ if ((mode == FileMode.Open || mode == FileMode.OpenOrCreate || mode == FileMode.Create || mode == FileMode.Truncate || mode == FileMode.Append) && exists)
{
var stream = new FatFileStream(FileSystem, this, fileId, fileAccess);
- if (mode == FileMode.Create)
+ if (mode == FileMode.Create || mode == FileMode.Truncate)
{
stream.SetLength(0);
}
+ else if (mode == FileMode.Append)
+ {
+ stream.Seek(0, SeekOrigin.End);
+ }
HandleAccessed(false);
@@ -257,8 +316,18 @@ internal FatFileStream OpenFile(FileName name, FileMode mode, FileAccess fileAcc
if ((mode == FileMode.OpenOrCreate || mode == FileMode.CreateNew || mode == FileMode.Create) && !exists)
{
+ FatFileName fatFileName;
+ try
+ {
+ fatFileName = FatFileName.FromName(name, FileSystem.FatOptions.FileNameEncodingTable, CheckIfShortNameExists);
+ }
+ catch (ArgumentException ex)
+ {
+ throw new IOException("Invalid file name", ex);
+ }
+
// Create new file
- var newEntry = new DirectoryEntry(FileSystem.FatOptions, name, FatAttributes.Archive,
+ var newEntry = new DirectoryEntry(FileSystem.FatOptions, fatFileName, FatAttributes.Archive,
FileSystem.FatVariant)
{
FirstCluster = 0, // i.e. Zero-length
@@ -278,25 +347,22 @@ internal FatFileStream OpenFile(FileName name, FileMode mode, FileAccess fileAcc
internal long AddEntry(DirectoryEntry newEntry)
{
// Unlink an entry from the free list (or add to the end of the existing directory)
- long pos;
- if (_freeEntries.Count > 0)
- {
- pos = _freeEntries[0];
- _freeEntries.RemoveAt(0);
- }
- else
+ var entryCount = newEntry.EntryCount;
+ var pos = _freeDirectoryEntryTable.Allocate(newEntry.EntryCount);
+
+ if (pos < 0)
{
pos = _endOfEntries;
- _endOfEntries += 32;
+ _endOfEntries += entryCount * DirectoryEntry.SizeOf;
}
// Put the new entry into it's slot
_dirStream.Position = pos;
- newEntry.WriteTo(_dirStream);
+ newEntry.WriteTo(_dirStream, FileSystem.FatOptions.FileNameEncodingTable);
// Update internal structures to reflect new entry (as if read from disk)
- _entries.Add(pos, newEntry);
-
+ AddEntryRaw(pos, newEntry);
+
HandleAccessed(forWrite: true);
return pos;
@@ -313,12 +379,10 @@ internal void DeleteEntry(long id, bool releaseContents)
{
var entry = _entries[id];
- var copy = new DirectoryEntry(entry)
- {
- Name = entry.Name.Deleted()
- };
+ var entryCount = entry.EntryCount;
+
_dirStream.Position = id;
- copy.WriteTo(_dirStream);
+ DirectoryEntry.WriteDeletedEntry(_dirStream, entryCount);
if (releaseContents)
{
@@ -326,8 +390,13 @@ internal void DeleteEntry(long id, bool releaseContents)
}
_entries.Remove(id);
- _freeEntries.Add(id);
+ _freeDirectoryEntryTable.AddFreeRange(id, entryCount);
+
+ // Remove from the short and full name lookup tables
+ _shortFileNameToEntry.Remove(entry.Name.ShortName);
+ _fullFileNameToEntry.Remove(entry.Name.FullName);
+
HandleAccessed(true);
}
finally
@@ -344,39 +413,47 @@ internal void UpdateEntry(long id, DirectoryEntry entry)
}
_dirStream.Position = id;
- entry.WriteTo(_dirStream);
+ entry.WriteTo(_dirStream, FileSystem.FatOptions.FileNameEncodingTable);
_entries[id] = entry;
}
- private void LoadEntries()
+ private void AddEntryRaw(long pos, DirectoryEntry entry)
{
- _entries = [];
- _freeEntries = [];
+ _entries.Add(pos, entry);
+ // Update the short and full name lookup tables
+ _shortFileNameToEntry.Add(entry.Name.ShortName, pos);
+ _fullFileNameToEntry.Add(entry.Name.FullName, pos);
+ }
+ private void LoadEntries()
+ {
_selfEntryLocation = -1;
_parentEntryLocation = -1;
+ long beginFreePosition = -1;
+ long endFreePosition = -1;
while (_dirStream.Position < _dirStream.Length)
{
+ var streamPos = _dirStream.Position;
var entry = new DirectoryEntry(FileSystem.FatOptions, _dirStream, FileSystem.FatVariant);
- var streamPos = _dirStream.Position - 32;
-
- if (entry.Attributes ==
- (FatAttributes.ReadOnly | FatAttributes.Hidden | FatAttributes.System | FatAttributes.VolumeId))
+
+ if ((entry.Attributes & FatAttributes.LongFileNameMask) == FatAttributes.LongFileName)
{
- // Long File Name entry
+ // Orphaned Long File Name entry
+ AddFreeEntry(streamPos);
}
else if (entry.Name.IsDeleted())
{
// E5 = Free Entry
- _freeEntries.Add(streamPos);
+ // LFN Orphane entries (that are not part of a valid entry) are also marked as free
+ AddFreeEntry(streamPos);
}
- else if (entry.Name == FileName.SelfEntryName)
+ else if (entry.Name == FatFileName.SelfEntryName)
{
_selfEntry = entry;
_selfEntryLocation = streamPos;
}
- else if (entry.Name == FileName.ParentEntryName)
+ else if (entry.Name == FatFileName.ParentEntryName)
{
_parentEntry = entry;
_parentEntryLocation = streamPos;
@@ -389,11 +466,36 @@ private void LoadEntries()
}
else
{
- _entries.Add(streamPos, entry);
+ AddEntryRaw(streamPos, entry);
}
}
+
+ // Record any pending free entry
+ AddFreeEntry(-1);
+
+ void AddFreeEntry(long pos)
+ {
+ if (beginFreePosition >= 0)
+ {
+ // If a free entry comes after the previous one, and is contiguous, extend the count
+ if (endFreePosition + DirectoryEntry.SizeOf == pos)
+ {
+ endFreePosition = pos;
+ return;
+ }
+
+ // Record any pending range
+ AddFreeRange(beginFreePosition, (int)((endFreePosition - beginFreePosition) / DirectoryEntry.SizeOf));
+ }
+
+ beginFreePosition = pos;
+ endFreePosition = pos + DirectoryEntry.SizeOf;
+ }
}
+ private void AddFreeRange(long position, int count)
+ => _freeDirectoryEntryTable.AddFreeRange(position, count);
+
private void HandleAccessed(bool forWrite)
{
if (FileSystem.CanWrite && _parent != null)
@@ -428,20 +530,16 @@ private void PopulateNewChildDirectory(DirectoryEntry newEntry)
using var stream = new ClusterStream(FileSystem, FileAccess.Write, newEntry.FirstCluster,
uint.MaxValue);
// First is the self-referencing entry...
- var selfEntry = new DirectoryEntry(newEntry)
- {
- Name = FileName.SelfEntryName
- };
- selfEntry.WriteTo(stream);
+ var selfEntry = new DirectoryEntry(newEntry, FatFileName.SelfEntryName);
+ selfEntry.WriteTo(stream, FileSystem.FatOptions.FileNameEncodingTable);
// Second is a clone of our self entry (i.e. parent) - though dates are odd...
- var parentEntry = new DirectoryEntry(SelfEntry)
+ var parentEntry = new DirectoryEntry(SelfEntry, FatFileName.ParentEntryName)
{
- Name = FileName.ParentEntryName,
CreationTime = newEntry.CreationTime,
LastWriteTime = newEntry.LastWriteTime
};
- parentEntry.WriteTo(stream);
+ parentEntry.WriteTo(stream, FileSystem.FatOptions.FileNameEncodingTable);
}
private void Dispose(bool disposing)
@@ -460,7 +558,7 @@ internal DirectoryEntry ParentsChildEntry
{
if (_parent == null)
{
- return new DirectoryEntry(FileSystem.FatOptions, FileName.ParentEntryName, FatAttributes.Directory,
+ return new DirectoryEntry(FileSystem.FatOptions, FatFileName.ParentEntryName, FatAttributes.Directory,
FileSystem.FatVariant);
}
@@ -477,7 +575,7 @@ internal DirectoryEntry SelfEntry
if (_parent == null)
{
// If we're the root directory, simulate the parent entry with a dummy record
- return new DirectoryEntry(FileSystem.FatOptions, FileName.Null, FatAttributes.Directory,
+ return new DirectoryEntry(FileSystem.FatOptions, FatFileName.Null, FatAttributes.Directory,
FileSystem.FatVariant);
}
@@ -489,7 +587,7 @@ internal DirectoryEntry SelfEntry
if (_selfEntryLocation >= 0)
{
_dirStream.Position = _selfEntryLocation;
- value.WriteTo(_dirStream);
+ value.WriteTo(_dirStream, FileSystem.FatOptions.FileNameEncodingTable);
_selfEntry = value;
}
}
@@ -507,7 +605,7 @@ internal DirectoryEntry ParentEntry
}
_dirStream.Position = _parentEntryLocation;
- value.WriteTo(_dirStream);
+ value.WriteTo(_dirStream, FileSystem.FatOptions.FileNameEncodingTable);
_parentEntry = value;
}
}
diff --git a/Library/DiscUtils.Fat/DirectoryEntry.cs b/Library/DiscUtils.Fat/DirectoryEntry.cs
index f739c7b7d..42cd35835 100644
--- a/Library/DiscUtils.Fat/DirectoryEntry.cs
+++ b/Library/DiscUtils.Fat/DirectoryEntry.cs
@@ -23,6 +23,7 @@
using System;
using System.Buffers;
using System.IO;
+using System.Text;
using DiscUtils.Streams;
using DiscUtils.Streams.Compatibility;
@@ -30,9 +31,15 @@ namespace DiscUtils.Fat;
internal class DirectoryEntry
{
+ ///
+ /// Size of a directory entry in bytes.
+ ///
+ public const int SizeOf = 32;
+
private readonly FatType _fatVariant;
private readonly FatFileSystemOptions _options;
- private byte _attr;
+ private FatFileName _name;
+ private FatAttributes _attr;
private ushort _creationDate;
private ushort _creationTime;
private byte _creationTimeTenth;
@@ -48,33 +55,39 @@ internal DirectoryEntry(FatFileSystemOptions options, Stream stream, FatType fat
_options = options;
_fatVariant = fatVariant;
- var bufferLength = 32;
+ var bufferLength = SizeOf;
var buffer = ArrayPool.Shared.Rent(bufferLength);
try
{
+ var initialPosition = stream.Position;
stream.ReadExactly(buffer, 0, bufferLength);
// LFN entry
- if ((buffer[0] & 0xc0) == 0x40 && buffer[11] == 0x0f)
+ if (FatFileName.TryGetLfnDirectoryEntryCount(buffer, out var lfnDirectoryEntryCount))
{
- var lfn_entries = buffer[0] & 0x3F;
-
- bufferLength += 32 * lfn_entries;
+ bufferLength += SizeOf * lfnDirectoryEntryCount;
if (buffer.Length < bufferLength)
{
var new_buffer = ArrayPool.Shared.Rent(bufferLength);
- System.Buffer.BlockCopy(buffer, 0, new_buffer, 0, 32);
+ System.Buffer.BlockCopy(buffer, 0, new_buffer, 0, SizeOf);
ArrayPool.Shared.Return(buffer);
buffer = new_buffer;
}
- stream.ReadExactly(buffer, 32, 32 * lfn_entries);
+ stream.ReadExactly(buffer, SizeOf, SizeOf * lfnDirectoryEntryCount);
}
- Load(buffer, 0, bufferLength);
+ Load(buffer, bufferLength, options.FileNameEncodingTable, out var bytesProcessed);
+
+ // If we have processed less than expected (because LFN entries were orphaned)
+ // seek to the actual position of what has been processed instead of what was prefetched above
+ if (bytesProcessed != bufferLength)
+ {
+ stream.Position = initialPosition + bytesProcessed;
+ }
}
finally
{
@@ -85,19 +98,19 @@ internal DirectoryEntry(FatFileSystemOptions options, Stream stream, FatType fat
}
}
- internal DirectoryEntry(FatFileSystemOptions options, FileName name, FatAttributes attrs, FatType fatVariant)
+ internal DirectoryEntry(FatFileSystemOptions options, in FatFileName name, FatAttributes attrs, FatType fatVariant)
{
- _options = options;
_fatVariant = fatVariant;
- Name = name;
- _attr = (byte)attrs;
+ _options = options;
+ _name = name;
+ _attr = attrs;
}
- internal DirectoryEntry(DirectoryEntry toCopy)
+ internal DirectoryEntry(DirectoryEntry toCopy, in FatFileName name)
{
- _options = toCopy._options;
_fatVariant = toCopy._fatVariant;
- Name = toCopy.Name;
+ _options = toCopy._options;
+ _name = name;
_attr = toCopy._attr;
_creationTimeTenth = toCopy._creationTimeTenth;
_creationTime = toCopy._creationTime;
@@ -109,10 +122,12 @@ internal DirectoryEntry(DirectoryEntry toCopy)
_fileSize = toCopy._fileSize;
}
+ public int EntryCount => 1 + Name.LfnDirectoryEntryCount;
+
public FatAttributes Attributes
{
- get => (FatAttributes)_attr;
- set => _attr = (byte)value;
+ get => _attr;
+ set => _attr = value;
}
public DateTime CreationTime
@@ -162,27 +177,50 @@ public DateTime LastWriteTime
set => DateTimeToFileTime(value, out _lastWriteDate, out _lastWriteTime);
}
- public FileName Name { get; set; }
+ public ref readonly FatFileName Name => ref _name;
- internal void WriteTo(Stream stream)
+ public void ReplaceShortName(string name, FastEncodingTable encodingTable)
{
- Span buffer = stackalloc byte[32];
-
- Name.GetBytes(buffer);
- buffer[11] = _attr;
- buffer[13] = _creationTimeTenth;
- EndianUtilities.WriteBytesLittleEndian(_creationTime, buffer.Slice(14));
- EndianUtilities.WriteBytesLittleEndian(_creationDate, buffer.Slice(16));
- EndianUtilities.WriteBytesLittleEndian(_lastAccessDate, buffer.Slice(18));
- EndianUtilities.WriteBytesLittleEndian(_firstClusterHi, buffer.Slice(20));
- EndianUtilities.WriteBytesLittleEndian(_lastWriteTime, buffer.Slice(22));
- EndianUtilities.WriteBytesLittleEndian(_lastWriteDate, buffer.Slice(24));
- EndianUtilities.WriteBytesLittleEndian(_firstClusterLo, buffer.Slice(26));
- EndianUtilities.WriteBytesLittleEndian(_fileSize, buffer.Slice(28));
+ try
+ {
+ _name = _name.ReplaceShortName(name, encodingTable);
+ }
+ catch (ArgumentException ex)
+ {
+ throw new IOException("Failed to replace short name", ex);
+ }
+ }
+
+ internal void WriteTo(Stream stream, FastEncodingTable encodingTable)
+ {
+ Span buffer = stackalloc byte[EntryCount * SizeOf];
+
+ Name.ToDirectoryEntryBytes(buffer, encodingTable);
+ int offset = buffer.Length - SizeOf;
+ buffer[offset + 11] = (byte)_attr;
+ buffer[offset + 13] = _creationTimeTenth;
+ EndianUtilities.WriteBytesLittleEndian(_creationTime, buffer.Slice(offset + 14));
+ EndianUtilities.WriteBytesLittleEndian(_creationDate, buffer.Slice(offset + 16));
+ EndianUtilities.WriteBytesLittleEndian(_lastAccessDate, buffer.Slice(offset + 18));
+ EndianUtilities.WriteBytesLittleEndian(_firstClusterHi, buffer.Slice(offset + 20));
+ EndianUtilities.WriteBytesLittleEndian(_lastWriteTime, buffer.Slice(offset + 22));
+ EndianUtilities.WriteBytesLittleEndian(_lastWriteDate, buffer.Slice(offset + 24));
+ EndianUtilities.WriteBytesLittleEndian(_firstClusterLo, buffer.Slice(offset + 26));
+ EndianUtilities.WriteBytesLittleEndian(_fileSize, buffer.Slice(offset + 28));
stream.Write(buffer);
}
+ public static void WriteDeletedEntry(Stream stream, int count)
+ {
+ var deletedBuffer = DeleteBuffer();
+ for (int i = 0; i < count; i++)
+ {
+ stream.Write(deletedBuffer);
+ }
+ static ReadOnlySpan DeleteBuffer() => [0xE5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ }
+
private static DateTime FileTimeToDateTime(ushort date, ushort time, byte tenths)
{
if (date is 0 or 0xFFFF)
@@ -226,21 +264,28 @@ private static void DateTimeToFileTime(DateTime value, out ushort date, out usho
tenths = (byte)(value.Second % 2 * 100 + value.Millisecond / 10);
}
- private void Load(byte[] data, int offset, int count)
+ private void Load(byte[] data, int count, FastEncodingTable fileNameEncoding, out int bytesProcessed)
{
- Name = new FileName(data.AsSpan(offset));
+ _name = FatFileName.FromDirectoryEntryBytes(data.AsSpan(0, count), fileNameEncoding, out bytesProcessed);
- offset += count - 32;
+ var offset = bytesProcessed - SizeOf;
+ _attr = (FatAttributes)data[offset + 11];
- _attr = data[offset + 11];
- _creationTimeTenth = data[offset + 13];
- _creationTime = EndianUtilities.ToUInt16LittleEndian(data, offset + 14);
- _creationDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 16);
- _lastAccessDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 18);
- _firstClusterHi = EndianUtilities.ToUInt16LittleEndian(data, offset + 20);
- _lastWriteTime = EndianUtilities.ToUInt16LittleEndian(data, offset + 22);
- _lastWriteDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 24);
- _firstClusterLo = EndianUtilities.ToUInt16LittleEndian(data, offset + 26);
- _fileSize = EndianUtilities.ToUInt32LittleEndian(data, offset + 28);
+ if (((_attr & FatAttributes.LongFileNameMask) == FatAttributes.LongFileName) || _name.IsDeleted())
+ {
+ // This is a deleted entry or an orphaned LFN entry, so we don't care about the other fields
+ }
+ else
+ {
+ _creationTimeTenth = data[offset + 13];
+ _creationTime = EndianUtilities.ToUInt16LittleEndian(data, offset + 14);
+ _creationDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 16);
+ _lastAccessDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 18);
+ _firstClusterHi = EndianUtilities.ToUInt16LittleEndian(data, offset + 20);
+ _lastWriteTime = EndianUtilities.ToUInt16LittleEndian(data, offset + 22);
+ _lastWriteDate = EndianUtilities.ToUInt16LittleEndian(data, offset + 24);
+ _firstClusterLo = EndianUtilities.ToUInt16LittleEndian(data, offset + 26);
+ _fileSize = EndianUtilities.ToUInt32LittleEndian(data, offset + 28);
+ }
}
}
\ No newline at end of file
diff --git a/Library/DiscUtils.Fat/DiscUtils.Fat.csproj b/Library/DiscUtils.Fat/DiscUtils.Fat.csproj
index bc787753c..6f00d4bc3 100644
--- a/Library/DiscUtils.Fat/DiscUtils.Fat.csproj
+++ b/Library/DiscUtils.Fat/DiscUtils.Fat.csproj
@@ -3,10 +3,13 @@
DiscUtils FAT filesystem parser
Kenneth Bell;LordMike;Olof Lagerkvist
DiscUtils;Filesystem;FAT
+ True
+
+
diff --git a/Library/DiscUtils.Fat/FastEncodingTable.cs b/Library/DiscUtils.Fat/FastEncodingTable.cs
new file mode 100644
index 000000000..acf6b5813
--- /dev/null
+++ b/Library/DiscUtils.Fat/FastEncodingTable.cs
@@ -0,0 +1,125 @@
+//
+// Copyright (c) 2024, Olof Lagerkvist and contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+#if NET8_0_OR_GREATER
+using System;
+using System.Collections.Frozen;
+#endif
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace DiscUtils.Fat;
+
+///
+/// Fast encoding 1-byte to 1-char table to help encoding/decoding short file names.
+///
+internal sealed unsafe class FastEncodingTable
+{
+ private readonly char[] _mapByteToChar;
+#if NET8_0_OR_GREATER
+ private readonly FrozenDictionary _mapUpperCharToByte;
+#else
+ private readonly Dictionary _mapUpperCharToByte;
+#endif
+
+#if !NETFRAMEWORK
+ static FastEncodingTable()
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ }
+#endif
+
+ ///
+ /// Gets the default encoding table for the IBM PC code page 437.
+ ///
+ public static readonly FastEncodingTable Default = new(Encoding.GetEncoding(437));
+
+ ///
+ /// Used to replace characters that cannot be encoded.
+ ///
+ public const char ReplacementChar = '\ufffd';
+
+ ///
+ ///
+ ///
+ ///
+ public FastEncodingTable(Encoding encoding)
+ {
+ Debug.Assert(encoding.IsSingleByte);
+ Encoding = encoding;
+
+ _mapByteToChar = new char[256];
+ var mapUpperCharToByte = new Dictionary(256);
+
+ // Calculate the mapping from byte to char
+ for (int i = 0; i < 256; ++i)
+ {
+ byte b = (byte)i;
+ char c = (char)0;
+ int encoded = encoding.GetChars(&b, 1, &c, 1);
+ if (encoded != 1)
+ {
+ c = ReplacementChar;
+ }
+ _mapByteToChar[i] = c;
+ mapUpperCharToByte[c] = b;
+ }
+
+ // Calculate the mapping from upper char to byte
+ for (int i = 0; i < 256; ++i)
+ {
+ var c = _mapByteToChar[i];
+ var upperChar = char.ToUpperInvariant(c);
+ if (mapUpperCharToByte.TryGetValue(upperChar, out var bUpper))
+ {
+ mapUpperCharToByte[c] = bUpper;
+ }
+ }
+#if NET8_0_OR_GREATER
+ _mapUpperCharToByte = mapUpperCharToByte.ToFrozenDictionary();
+#else
+ _mapUpperCharToByte = mapUpperCharToByte;
+#endif
+ }
+
+ ///
+ /// The encoding used to create the table.
+ ///
+ public Encoding Encoding { get; }
+
+ ///
+ /// Gets the char for the specified byte.
+ ///
+ /// The byte to convert.
+ /// The char converted or if no mapping exists.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public char GetCharFromByte(byte b) => _mapByteToChar[(int)b];
+
+ ///
+ /// Gets the upper case byte for the specified char.
+ ///
+ /// The char to convert.
+ /// The upper case char converted if this method returns true
+ /// true if the char was converted; otherwise, false.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetCharToByteUpperCase(char c, out byte b) => _mapUpperCharToByte.TryGetValue(c, out b);
+}
\ No newline at end of file
diff --git a/Library/DiscUtils.Fat/FatAttributes.cs b/Library/DiscUtils.Fat/FatAttributes.cs
index 303026656..edde14ecb 100644
--- a/Library/DiscUtils.Fat/FatAttributes.cs
+++ b/Library/DiscUtils.Fat/FatAttributes.cs
@@ -32,5 +32,7 @@ internal enum FatAttributes : byte
System = 0x04,
VolumeId = 0x08,
Directory = 0x10,
- Archive = 0x20
+ Archive = 0x20,
+ LongFileName = 0x0F,
+ LongFileNameMask = 0x3F,
}
\ No newline at end of file
diff --git a/Library/DiscUtils.Fat/FatFileName.cs b/Library/DiscUtils.Fat/FatFileName.cs
new file mode 100644
index 000000000..a2b1abdea
--- /dev/null
+++ b/Library/DiscUtils.Fat/FatFileName.cs
@@ -0,0 +1,959 @@
+//
+// Copyright (c) 2008-2011, Kenneth Bell
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using DiscUtils.Internal;
+using DiscUtils.Streams;
+using DiscUtils.Streams.Compatibility;
+
+namespace DiscUtils.Fat;
+
+///
+/// Implementation for reading the name of a file in a FAT directory entry.
+///
+///
+/// http://elm-chan.org/docs/fat_e.html
+/// https://en.wikipedia.org/wiki/8.3_filename
+/// Long File Name support (lfn) in VFAT is described at https://www.kernel.org/doc/Documentation/filesystems/vfat.txt
+/// Other documentation used: https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system
+///
+internal struct FatFileName : IEquatable
+{
+ private const byte SpaceByte = 0x20;
+
+ const string InvalidCharsForShortName = "\"*+,./:;<=>?[\\]|";
+
+ public static readonly FatFileName SelfEntryName = new(".");
+
+ public static readonly FatFileName ParentEntryName = new("..");
+
+ public static readonly FatFileName Null = new("\0\0\0\0\0\0\0\0\0\0\0\0");
+
+ private readonly string _shortName; // null indicates a deleted / orphaned entry
+
+ private readonly string _longName;
+
+ private FatFileName(string shortName, string longName = null)
+ {
+ _shortName = shortName;
+ _longName = longName;
+ }
+
+ public readonly string ShortName => _shortName;
+
+ public readonly string LongName => _longName;
+
+ public readonly string FullName => _longName ?? _shortName;
+
+ ///
+ /// Gets the number of additional directory entries used by the long file name.
+ ///
+ public readonly int LfnDirectoryEntryCount => _longName is not null ? (_longName.Length + 12) / 13 : 0;
+
+ public readonly bool Equals(FatFileName other) => Equals(this, other);
+
+ public static bool Equals(in FatFileName a, in FatFileName b)
+ {
+ if (a._longName != null)
+ {
+ if (StringComparer.OrdinalIgnoreCase.Equals(a._longName, b._longName) ||
+ StringComparer.OrdinalIgnoreCase.Equals(a._longName, b._shortName))
+ {
+ return true;
+ }
+ }
+ else if (b._longName != null)
+ {
+ if (StringComparer.OrdinalIgnoreCase.Equals(a._shortName, b._longName))
+ {
+ return true;
+ }
+ }
+ else
+ {
+ if (StringComparer.OrdinalIgnoreCase.Compare(a._shortName, b._shortName) == 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public readonly FatFileName ReplaceShortName(string name, FastEncodingTable encodingTable)
+ {
+ if (name is null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ var idx = 0;
+ var indexOfDot = -1;
+
+ for (var i = 0; i < name.Length; i++)
+ {
+ if (idx >= 11)
+ {
+ throw new ArgumentException($"File name too long '{name}'", nameof(name));
+ }
+
+ var c = name[i];
+
+ if (c < 0x20)
+ {
+ throw new ArgumentException($"Invalid control character at index {i} in short name '{name}'", nameof(name));
+ }
+
+ if (c == '.')
+ {
+ if (indexOfDot >= 0)
+ {
+ throw new ArgumentException($"Multiple dots at index {i} in short name '{name}'", nameof(name));
+ }
+
+ indexOfDot = idx;
+ }
+ else if (c == ' ')
+ {
+ throw new ArgumentException($"Invalid space character at index {i} in short name '{name}'", nameof(name));
+ }
+ else if (InvalidCharsForShortName.IndexOf(c) >= 0 || !encodingTable.TryGetCharToByteUpperCase(c, out _))
+ {
+ throw new ArgumentException($"Invalid character `{c}` at index {i} in short name '{name}'", nameof(name));
+ }
+
+ idx++;
+ }
+
+ if (idx == 0)
+ {
+ throw new ArgumentException("Empty file name", nameof(name));
+ }
+
+ if (indexOfDot > 0 && idx - indexOfDot > 4)
+ {
+ throw new ArgumentException($"Extension too long in short name '{name}'", nameof(name));
+ }
+
+ return new(name, _longName);
+ }
+
+ public readonly bool IsMatch(Func filter)
+ {
+ var searchName = FullName;
+ if (searchName is null)
+ {
+ return false;
+ }
+
+ if (searchName.IndexOf('.') < 0)
+ {
+ searchName += '.';
+ }
+
+ return filter(searchName);
+ }
+
+ public readonly bool IsDeleted() => _shortName is null;
+
+ public readonly bool IsEndMarker() => _shortName is not null && _shortName.Equals(Null._shortName, StringComparison.Ordinal);
+
+ public readonly override bool Equals(object other)
+ => other is FatFileName otherName && Equals(this, otherName);
+
+ public readonly override int GetHashCode()
+ {
+ var displayName = FullName;
+ return displayName != null ? displayName.GetHashCode() : 0;
+ }
+
+ public readonly override string ToString() => FullName;
+
+ ///
+ /// Writes this to a buffer as proper directory entries headers for the long file name and the short file name.
+ ///
+ /// The buffer to receive the directory entries initialized with the long file name and the short file name.
+ /// Encoding table
+ public readonly void ToDirectoryEntryBytes(Span buffer, FastEncodingTable encodingTable)
+ {
+ if (_shortName is null) throw new InvalidOperationException("Cannot write a deleted file name");
+
+ if (_shortName.Equals(Null._shortName, StringComparison.Ordinal))
+ {
+ buffer.Fill(0x00);
+ return;
+ }
+
+ var lfnCount = LfnDirectoryEntryCount;
+ if (buffer.Length < (lfnCount + 1) * DirectoryEntry.SizeOf)
+ {
+ throw new ArgumentException("Buffer is too small", nameof(buffer));
+ }
+
+ // Make sure that the directory entry is fully initialized to 0
+ var offsetToSfnEntry = lfnCount * DirectoryEntry.SizeOf;
+ var sfnEntry = buffer.Slice(offsetToSfnEntry, DirectoryEntry.SizeOf);
+ sfnEntry.Slice(12).Fill(0);
+
+ var finalBytes = sfnEntry.Slice(0, 11);
+ finalBytes.Fill((byte)' ');
+
+ // Initialize the buffer with the short name
+ var indexOfDot = _shortName.IndexOf('.');
+ int idxFinal = 0;
+
+ var baseNameLength = indexOfDot > 0 ? indexOfDot : _shortName.Length;
+
+ // Process the base name (at max 8 characters)
+ bool hasLowerCaseBase = false;
+
+ for (var i = 0; i < baseNameLength; i++, idxFinal++)
+ {
+ var c = _shortName[i];
+
+ // We have the guarantee if the short name that if there is a lower case base name character, then all letters are lower case
+ if (char.IsLetter(c) && char.IsLower(c))
+ {
+ hasLowerCaseBase = true;
+ }
+
+ // Make sure that we convert it back to upper case
+ var isValidChar = encodingTable.TryGetCharToByteUpperCase(c, out finalBytes[idxFinal]);
+ Debug.Assert(isValidChar);
+ }
+
+ // Mark the base name as lower case if needed
+ if (hasLowerCaseBase)
+ {
+ sfnEntry[12] |= 1 << 3;
+ }
+
+ // Copy the extension part
+ if (indexOfDot > 0)
+ {
+ var hasLowerCaseExt = false;
+ idxFinal = 8;
+ for (int i = indexOfDot + 1; i < _shortName.Length; i++, idxFinal++)
+ {
+ var c = _shortName[i];
+
+ if (char.IsLetter(c) && char.IsLower(c))
+ {
+ hasLowerCaseExt = true;
+ }
+
+ // Make sure that we convert it back to upper case
+ var isValidChar = encodingTable.TryGetCharToByteUpperCase(c, out finalBytes[idxFinal]);
+ Debug.Assert(isValidChar);
+ }
+
+ // Mark the extension as lower case if needed
+ if (hasLowerCaseExt)
+ {
+ sfnEntry[12] |= 1 << 4;
+ }
+ }
+
+ var offset = 0;
+ if (_longName != null)
+ {
+ // Calculate the checksum for the short name
+ var checksumByte = SfnChecksum(finalBytes);
+
+ var lfnBytes = MemoryMarshal.AsBytes(_longName.AsSpan());
+
+ var length13 = _longName.Length % 13;
+
+ for (var i = lfnCount; i > 0; i--, offset += DirectoryEntry.SizeOf)
+ {
+ if (i == lfnCount && length13 > 0)
+ {
+ // We fill all characters with 0xff (other entries will be filled with characters or initialized below)
+
+ var remainingLength = length13;
+ buffer.Slice(offset, DirectoryEntry.SizeOf).Fill(0xff);
+
+ // TODO: This code is not endian-safe. Long file names will be messed up when this code runs on big endian machines.
+ var nextLength = Math.Min(length13, 5);
+
+ var localOffset = offset + 1;
+ lfnBytes.Slice(26 * (i - 1), nextLength * 2).CopyTo(buffer.Slice(localOffset));
+ localOffset += nextLength * 2;
+ remainingLength -= nextLength;
+ if (remainingLength > 0)
+ {
+ localOffset = offset + 14;
+ nextLength = Math.Min(length13 - 5, 6);
+ lfnBytes.Slice(26 * (i - 1) + 10, nextLength * 2).CopyTo(buffer.Slice(localOffset));
+ localOffset += nextLength * 2;
+ remainingLength -= nextLength;
+ if (remainingLength > 0)
+ {
+ localOffset = offset + 28;
+ nextLength = Math.Min(length13 - 11, 2);
+ lfnBytes.Slice(26 * (i - 1) + 22, nextLength * 2).CopyTo(buffer.Slice(localOffset));
+ localOffset += nextLength * 2;
+ }
+ else if (localOffset == offset + 26)
+ {
+ // We need to write the null terminator on the next field name
+ localOffset = offset + 28;
+ }
+ }
+ else if (localOffset == offset + 11)
+ {
+ // We need to write the null terminator on the next field name
+ localOffset = offset + 14;
+ }
+
+ buffer[localOffset] = 0;
+ buffer[localOffset + 1] = 0;
+ }
+ else
+ {
+ // TODO: This code is not endian-safe. Long file names will be messed up when this code runs on big endian machines.
+ lfnBytes.Slice(26 * (i - 1), 10).CopyTo(buffer.Slice(offset + 1));
+ lfnBytes.Slice(26 * (i - 1) + 10, 12).CopyTo(buffer.Slice(offset + 14));
+ lfnBytes.Slice(26 * (i - 1) + 22, 4).CopyTo(buffer.Slice(offset + 28));
+ }
+
+ var seq = (byte)i;
+ if (i == lfnCount)
+ {
+ seq |= 0x40;
+ }
+
+ // Initialize the fields of the Directory Entry
+ buffer[offset] = seq;
+ buffer[offset + 11] = 0x0f;
+ buffer[offset + 12] = 0;
+ buffer[offset + 13] = checksumByte;
+ buffer[offset + 26] = 0;
+ buffer[offset + 27] = 0;
+ }
+ }
+ }
+
+ ///
+ /// Create a new from a long name.
+ ///
+ /// The file name to encode.
+ /// The encoding table to use.
+ /// A function that validates if short name is already used.
+ /// The instance.
+ public static unsafe FatFileName FromName(string name, FastEncodingTable encodingTable, Func shortNameExistFunction)
+ {
+#if NET6_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(name);
+#else
+ if (name is null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+#endif
+
+ if (name.Length > 255)
+ {
+ throw new IOException($"Too long name: '{name}'");
+ }
+
+ if (name.Length == 0)
+ {
+ throw new ArgumentException("Empty file name", nameof(name));
+ }
+
+ ValidateCharsFromLongName(name);
+
+ var shortNameBytes = stackalloc byte[12];
+
+ // Trailing . are entirely removed from the original long name
+ name = name.TrimEnd('.');
+
+ if (name.Length == 0)
+ {
+ throw new ArgumentException("A file name cannot contain only '.' in its name", nameof(name));
+ }
+
+ // Now we are going to process the long name to generate the short name
+ var nameSpan = name.AsSpan();
+ // Strip all leading and embedded spaces from the long name.
+ nameSpan = nameSpan.TrimStart(' ');
+ // Strip all leading periods from the long name.
+ nameSpan = nameSpan.TrimStart('.');
+
+ // Spot the last dot in the name for the extension
+ var indexOfDot = nameSpan.LastIndexOf('.');
+ var baseLength = indexOfDot;
+ if (baseLength < 0)
+ {
+ baseLength = nameSpan.Length;
+ }
+
+ // If we have trimmed already the name, this is a lossy conversion to short name
+ var lossy = nameSpan.Length != name.Length;
+ var hasNonSupportedChar = lossy;
+ var isBaseAllUpper = true;
+ var isBaseAllLower = true;
+ var shortLength = 0;
+
+ // Process the base name (at max 8 characters)
+ ProcessChars(nameSpan, 0, baseLength, 8, ref lossy, ref isBaseAllUpper, ref isBaseAllLower, shortNameBytes, ref shortLength, ref hasNonSupportedChar, encodingTable);
+
+ if (shortLength == 0)
+ {
+ lossy = true;
+ }
+
+ // Process the extension (at max 3 characters)
+ var baseNameLength = shortLength;
+ var extensionStart = baseNameLength;
+
+ var isExtensionAllUpper = true;
+ var isExtensionAllLower = true;
+ if (indexOfDot > 0)
+ {
+ // In case of a base name with non-supported encoding characters, we will append a hash
+ if (hasNonSupportedChar && shortLength <= 2)
+ {
+ var hash = LfnHash(name);
+ shortLength = Math.Min(shortLength, 2);
+ ConvertToHex(hash, new Span(shortNameBytes + shortLength, 4));
+ shortLength += 4;
+
+ baseNameLength = shortLength;
+ extensionStart = baseNameLength;
+ }
+
+ // 6. Insert a dot at the end of the primary components of the basis-name iff the basis name has an extension after the last period in the name.
+ shortNameBytes[shortLength++] = (byte)'.';
+ var unused = false;
+ ProcessChars(nameSpan, indexOfDot + 1, nameSpan.Length, 3, ref lossy, ref isExtensionAllUpper, ref isExtensionAllLower, shortNameBytes, ref shortLength, ref unused, encodingTable);
+ }
+
+ // Initialize the temp buffer with the short name (that will be used to generate the short name and handle collisions)
+ var tempBytes = stackalloc byte[12];
+ var tempLength = shortLength;
+ for (var i = 0; i < shortLength; i++)
+ {
+ tempBytes[i] = shortNameBytes[i];
+ }
+
+ // If we have mixed characters in the base name, we need to use the long name
+ // If we have mixed characters in the extension, we need to use the long name
+ // Mixed cases shortname without lossy means that we might not need to append the numeric tail e.g ~1
+ // So differentiate this case from lossy
+ var mixedCases = (!isBaseAllLower && !isBaseAllUpper) || (!isExtensionAllLower && !isExtensionAllUpper);
+
+ // If the short name is not lossy, then we can keep the state of lower/upper case in the shortname
+ // as we will use the byte 0x0c in the directory entry to store this information
+ if (!lossy && !mixedCases)
+ {
+ if (isBaseAllLower)
+ {
+ for(var i = 0; i < baseNameLength; i++)
+ {
+ tempBytes[i] = (byte)char.ToLowerInvariant((char)tempBytes[i]);
+ }
+ }
+
+ if (isExtensionAllLower)
+ {
+ for (var i = extensionStart; i < shortLength; i++)
+ {
+ tempBytes[i] = (byte)char.ToLowerInvariant((char)tempBytes[i]);
+ }
+ }
+ }
+
+ // The "~n" string can range from "~1" to "~999999".
+ const int MaxTailNumber = 999999;
+ var tailNumberLengthAscii = 1;
+ var tailNumber = 1;
+ var forceTrailingOnFirstLossy = lossy;
+ var bufferChar = stackalloc char[12];
+ var encoding = encodingTable.Encoding;
+
+ while (tailNumber <= MaxTailNumber)
+ {
+ var encodedShortNameChar = encoding.GetChars(tempBytes, tempLength, bufferChar, 12);
+ var shortName = new string(bufferChar, 0, encodedShortNameChar);
+
+ if (forceTrailingOnFirstLossy || shortNameExistFunction(shortName))
+ {
+ lossy = true;
+ forceTrailingOnFirstLossy = false;
+
+ // From https://en.wikipedia.org/wiki/8.3_filename
+ // If at least 4 files or folders already exist with the same extension and first 6 characters in their short names,
+ // the stripped LFN is instead truncated to the first 2 letters of the basename (or 1 if the basename has only 1 letter),
+ // followed by 4 hexadecimal digits derived from an undocumented hash of the filename
+ if (!hasNonSupportedChar && tailNumber > 4)
+ {
+ var hash = LfnHash(name);
+
+ // At max 2 characters
+ baseNameLength = Math.Min(baseNameLength, 2);
+ for (var i = 0; i < baseNameLength; i++)
+ {
+ tempBytes[i] = shortNameBytes[i];
+ }
+
+ // 4 hexadecimal digits
+ ConvertToHex(hash, new Span(tempBytes + baseNameLength, 4));
+ baseNameLength += 4;
+
+ hasNonSupportedChar = true;
+
+ // Reset numbering
+ tailNumberLengthAscii = 1;
+ tailNumber = 1;
+ }
+
+ var trailCount = 1 + tailNumberLengthAscii;
+ var maxTrail = 8 - trailCount;
+ if (baseNameLength > maxTrail)
+ {
+ baseNameLength = maxTrail;
+ }
+
+ // Append the numeric tail ~n
+ var remaining = tailNumber;
+ var idx = baseNameLength + trailCount - 1;
+ while (remaining > 0)
+ {
+ remaining = Math.DivRem(remaining, 10, out var digit);
+ tempBytes[idx] = (byte)('0' + digit);
+ idx--;
+ }
+ tempBytes[idx] = (byte)'~';
+
+ // Append the extension
+ idx = baseNameLength + trailCount;
+ for (var i = extensionStart; i < shortLength; i++, idx++)
+ {
+ tempBytes[idx] = shortNameBytes[i];
+ }
+ tempLength = idx;
+
+ // Increment the numeric tail number for the next round
+ tailNumber++;
+ tailNumberLengthAscii += (tailNumber % 10) == 0 ? 1 : 0;
+ }
+ else
+ {
+ return new(shortName, lossy || mixedCases ? name : null);
+ }
+ }
+
+ throw new IOException($"Too many files with the same name '{name}'");
+ }
+
+ ///
+ /// Create a new from a serialized byte array of directory entries.
+ ///
+ /// The buffer to decode the filename from.
+ /// The encoding table to use to decode shortname.
+ /// The amount of data actually processed from buffer.
+ ///
+ public static unsafe FatFileName FromDirectoryEntryBytes(ReadOnlySpan data, FastEncodingTable encodingTable, out int offset)
+ {
+ offset = 0;
+
+ string longName = null;
+
+ // LFN
+ if (TryGetLfnDirectoryEntryCount(data, out var lfnDirectoryEntryCount) && lfnDirectoryEntryCount > 0)
+ {
+ // Update the number of directory entries used
+ Span lfn_chars = stackalloc char[13 * lfnDirectoryEntryCount];
+
+ lfn_chars.Clear();
+
+ var lfn_bytes = MemoryMarshal.AsBytes(lfn_chars);
+
+ for (var i = lfnDirectoryEntryCount; i > 0 ; i--, offset += DirectoryEntry.SizeOf)
+ {
+ if ((data[offset] & 0x3f) == i && ((FatAttributes)data[offset + 11] & FatAttributes.LongFileNameMask) == FatAttributes.LongFileName)
+ {
+ // TODO: This code is not endian-safe. Long file names will be messed up when this code runs on big endian machines.
+ data.Slice(offset + 1, 10).CopyTo(lfn_bytes.Slice(26 * (i - 1)));
+ data.Slice(offset + 14, 12).CopyTo(lfn_bytes.Slice(26 * (i - 1) + 10));
+ data.Slice(offset + 28, 4).CopyTo(lfn_bytes.Slice(26 * (i - 1) + 22));
+ }
+ else
+ {
+ // We have an orphaned long file name entry, we need to reset to the first entry and skip it
+ offset = DirectoryEntry.SizeOf; // inform that we only processed one entry
+ return new FatFileName(null, null);
+ }
+ }
+
+ var nullpos = lfn_chars.IndexOf('\0');
+ if (nullpos < 0)
+ {
+ nullpos = lfn_chars.Length;
+ }
+
+ longName = lfn_chars.Slice(0, nullpos).ToString();
+ }
+
+ // If we still have long file name entry, this is invalid
+ if (((FatAttributes)data[offset + 11] & FatAttributes.LongFileNameMask) == FatAttributes.LongFileName)
+ {
+ // We have an orphaned long file name entry, we need to reset to the first entry and skip it
+ offset = DirectoryEntry.SizeOf; // inform that we only processed one entry
+ return new(null, null);
+ }
+
+ // Check if the shortname is entirely zeroed
+ bool isNull = MemoryMarshal.Cast(data.Slice(0, 8))[0] == 0
+ && MemoryMarshal.Cast(data.Slice(11 - 4, 4))[0] == 0;
+ if (isNull)
+ {
+ offset = DirectoryEntry.SizeOf; // inform that we only processed one entry
+ return Null;
+ }
+
+ // Verifies the checksum of the long file name entries
+ if (lfnDirectoryEntryCount > 0)
+ {
+ var checksum = SfnChecksum(data.Slice(offset, 11));
+ for (int i = 0; i < lfnDirectoryEntryCount; i++)
+ {
+ var lfnChecksum = data[i * DirectoryEntry.SizeOf + 13];
+ if (lfnChecksum != checksum)
+ {
+ // We have an orphaned long file name entry, we need to reset to the first entry and skip it
+ offset = DirectoryEntry.SizeOf; // inform that we only processed one entry
+ return new(null, null);
+ }
+ }
+ }
+
+ // Deleted entry
+ if (data[offset] == 0xe5)
+ {
+ offset += DirectoryEntry.SizeOf;
+ return new(null, null);
+ }
+
+ // Process the name part in the short name 8.3
+ var tmpBuffer = stackalloc byte[12]; // 8.3 + including `.`
+ int tmpLength = 0;
+ for (var i = 0; i < 8; i++)
+ {
+ var b = data[offset + i];
+ if (b == (byte)' ') // trimming space
+ {
+ break;
+ }
+
+ tmpBuffer[tmpLength++] = b;
+ }
+
+ int nameLength = tmpLength;
+
+ // Process the extension part in the short name 8.3
+ var hasExtension = false;
+
+ for (var i = 0; i < 3; i++)
+ {
+ var b = data[offset + 8 + i];
+ if (b == (byte)' ') // trimming space
+ {
+ break;
+ }
+
+ if (!hasExtension)
+ {
+ tmpBuffer[tmpLength++] = (byte)'.'; // add dot
+ hasExtension = true;
+ }
+
+ tmpBuffer[tmpLength++] = b;
+ }
+
+ Debug.Assert(tmpLength > 0);
+
+ string shortName = null;
+ if (tmpLength == 1 && tmpBuffer[0] == '.')
+ {
+ Debug.Assert(longName is null);
+ shortName = ".";
+ }
+ else if (tmpLength == 2 && tmpBuffer[0] == '.' && tmpBuffer[1] == '.')
+ {
+ Debug.Assert(longName is null);
+ shortName = "..";
+ }
+ else
+ {
+ // The character 0xE5 is a valid character but it is used to mark a deleted file
+ // So the character 0x05 is used to represent the character 0xE5
+ // When reading back we need to convert it back to 0xE5
+ if (tmpBuffer[0] == 0x05)
+ {
+ tmpBuffer[0] = 0xe5;
+ }
+
+ // Bits 3 and 4 in offset 12 of the directory entry indicate if the name and extension are lowercase
+ var d0C = data[offset + 12];
+ var nameIsLowercase = (d0C & (1 << 3)) != 0;
+ var extIsLowercase = (d0C & (1 << 4)) != 0;
+
+ var utf16Buffer = stackalloc char[12];
+ var utf16Length = encodingTable.Encoding.GetChars(tmpBuffer, tmpLength, utf16Buffer, 12);
+ Debug.Assert(utf16Length == tmpLength);
+
+ // We apply case information recovered from the directory entry
+ if (nameIsLowercase || extIsLowercase)
+ {
+ if (nameIsLowercase)
+ {
+ for (var i = 0; i < nameLength; i++)
+ {
+ utf16Buffer[i] = char.ToLowerInvariant(utf16Buffer[i]);
+ }
+ }
+
+ if (extIsLowercase)
+ {
+ for (var i = nameLength + 1; i < tmpLength; i++)
+ {
+ utf16Buffer[i] = char.ToLowerInvariant(utf16Buffer[i]);
+ }
+ }
+ }
+
+ shortName = new string(utf16Buffer, 0, utf16Length);
+ }
+
+ offset += DirectoryEntry.SizeOf;
+ return new(shortName, longName);
+ }
+
+ ///
+ /// Create a new from a path.
+ ///
+ /// The parent directory to create the entry
+ ///
+ ///
+ ///
+ public static FatFileName NewPath(Directory parentDir, string path, FastEncodingTable encodingTable) => FromName(Utilities.GetFileFromPath(path), encodingTable, parentDir.CheckIfShortNameExists);
+
+ ///
+ /// Trie to get the number of additional directory entries used by the long file name.
+ ///
+ /// The buffer containing the first directory entry to process.
+ /// The number of additional long file name directory entries if this function returns true.
+ /// true if the buffer contains a long file name directory entry, false otherwise.
+ public static bool TryGetLfnDirectoryEntryCount(ReadOnlySpan data, out int count)
+ {
+ count = 0;
+
+ var d0 = data[0];
+ if ((d0 & 0xc0) == 0x40 && data[11] == 0x0f)
+ {
+ count = d0 & 0x3f;
+ return true;
+ }
+
+ return false;
+ }
+
+ public static bool operator ==(FatFileName a, FatFileName b) => Equals(a, b);
+
+ public static bool operator !=(FatFileName a, FatFileName b) => !Equals(a, b);
+
+ private static bool Contains(byte[] array, byte val) => Array.IndexOf(array, val) >= 0;
+
+ ///
+ /// Verify that the name does not contain invalid characters.
+ ///
+ /// The name to verify.
+ /// The name contains invalid characters.
+ private static void ValidateCharsFromLongName(string name)
+ {
+ // The invalid chars are composed of the InvalidCharsForShortName minus +,;=[]
+ const string invalidCharsForLongName = "\"*/:<>?\\|";
+
+ // Check invalid chars
+ var indexOfInvalidChar = name.AsSpan().IndexOfAny(invalidCharsForLongName.AsSpan());
+ if (indexOfInvalidChar > 0)
+ {
+ throw new ArgumentException($"Invalid character in file name '{name[indexOfInvalidChar]}'", nameof(name));
+ }
+
+ for (var i = 0; i < name.Length; i++)
+ {
+ if (name[i] < 0x20)
+ {
+ throw new ArgumentException($"Invalid character in file name '{name[i]}'", nameof(name));
+ }
+ }
+ }
+
+ ///
+ /// Function used by to process the characters of the long name and compute the short name for both the base name and the extension.
+ ///
+ private static unsafe void ProcessChars(ReadOnlySpan name, int start, int end, int maxCount, ref bool lossy, ref bool isBaseAllUpper, ref bool isBaseAllLower, byte* shortNameBytes, ref int shortLength, ref bool hasNonSupportedChar, FastEncodingTable encodingTable)
+ {
+ for (int i = start; i < end; i++)
+ {
+ if (i - start == maxCount)
+ {
+ lossy = true;
+ break;
+ }
+
+ var c = name[i];
+
+ // 3. Strip all leading and embedded spaces from the long name.
+ if (c == ' ')
+ {
+ hasNonSupportedChar = true;
+ lossy = true;
+ continue;
+ }
+
+ if (c == '.')
+ {
+ lossy = true;
+ continue;
+ }
+
+ if (char.IsLetter(c))
+ {
+ if (!char.IsUpper(c))
+ {
+ isBaseAllUpper = false;
+ }
+
+ if (!char.IsLower(c))
+ {
+ isBaseAllLower = false;
+ }
+ }
+
+ if (InvalidCharsForShortName.IndexOf(c) >= 0)
+ {
+ lossy = true;
+ shortNameBytes[shortLength] = (byte)'_';
+ }
+ else
+ {
+ if (!encodingTable.TryGetCharToByteUpperCase(c, out var b))
+ {
+ hasNonSupportedChar = true;
+ lossy = true;
+ continue;
+ }
+ shortNameBytes[shortLength] = b;
+ }
+
+ shortLength++;
+ }
+ }
+
+ ///
+ /// Calculates the hash for a long file name.
+ ///
+ /// The long filename
+ /// The u16 hash
+ private static ushort LfnHash(string name)
+ {
+ // Amazing work from Tom Galvin to recover this algorithm
+ // http://tomgalvin.uk/blog/gen/2015/06/09/filenames/
+ // https://web.archive.org/web/20230825135743/https://tomgalvin.uk/blog/gen/2015/06/09/filenames/
+ ushort checksum = 0;
+
+ for (int i = 0; i < name.Length; i++)
+ {
+ checksum = (ushort)(checksum * 0x25 + name[i]);
+ }
+
+ int temp = checksum * 314159269;
+ if (temp < 0) temp = -temp;
+ temp -= (int)(((ulong)((long)temp * 1152921497) >> 60) * 1000000007);
+ checksum = (ushort)temp;
+
+ // reverse nibble order
+ checksum = (ushort)(
+ ((checksum & 0xf000) >> 12) |
+ ((checksum & 0x0f00) >> 4) |
+ ((checksum & 0x00f0) << 4) |
+ ((checksum & 0x000f) << 12));
+
+ return checksum;
+ }
+
+ ///
+ /// Calculate the checksum for a short file name.
+ ///
+ /// The short file name
+ /// The checksum
+ private static byte SfnChecksum(ReadOnlySpan shortName)
+ {
+ byte sum = 0;
+ for (var i = 0; i < shortName.Length; i++)
+ {
+ sum = (byte)((sum << 7) + (sum >> 1) + shortName[i]);
+ }
+ return sum;
+ }
+
+ private static void ConvertToHex(ushort value, Span buffer)
+ {
+ buffer[0] = ByteToHex((value >> 12) & 0x0f);
+ buffer[1] = ByteToHex((value >> 8) & 0x0f);
+ buffer[2] = ByteToHex((value >> 4) & 0x0f);
+ buffer[3] = ByteToHex(value & 0x0f);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static byte ByteToHex(int b) => Unsafe.Add(ref MemoryMarshal.GetReference(ByteToHexLookup), b);
+
+ private static ReadOnlySpan ByteToHexLookup =>
+ [
+ (byte)'0',
+ (byte)'1',
+ (byte)'2',
+ (byte)'3',
+ (byte)'4',
+ (byte)'5',
+ (byte)'6',
+ (byte)'7',
+ (byte)'8',
+ (byte)'9',
+ (byte)'A',
+ (byte)'B',
+ (byte)'C',
+ (byte)'D',
+ (byte)'E',
+ (byte)'F',
+ ];
+}
\ No newline at end of file
diff --git a/Library/DiscUtils.Fat/FatFileSystem.cs b/Library/DiscUtils.Fat/FatFileSystem.cs
index 0fdf20f3d..5c2e075cb 100644
--- a/Library/DiscUtils.Fat/FatFileSystem.cs
+++ b/Library/DiscUtils.Fat/FatFileSystem.cs
@@ -326,7 +326,7 @@ public override string VolumeLabel
return _bsVolLab;
}
- return _rootDir.GetEntry(volId).Name.GetRawName(FatOptions.FileNameEncoding);
+ return _rootDir.GetEntry(volId).Name.ShortName;
}
}
@@ -396,7 +396,7 @@ public override SparseStream OpenFile(string path, FileMode mode, FileAccess acc
if (entryId < 0)
{
- return parent.OpenFile(FileName.FromPath(path, FatOptions.FileNameEncoding), mode, access);
+ return parent.OpenFile(Utilities.GetFileFromPath(path), mode, access);
}
var dirEntry = parent.GetEntry(entryId);
@@ -406,7 +406,7 @@ public override SparseStream OpenFile(string path, FileMode mode, FileAccess acc
throw new IOException("Attempt to open directory as a file");
}
- return parent.OpenFile(dirEntry.Name, mode, access);
+ return parent.OpenFile(dirEntry.Name.FullName, mode, access);
}
public IEnumerable PathToExtents(string path)
@@ -557,8 +557,8 @@ public string GetShortName(string path)
{
return null;
}
-
- return GetDirectoryEntry(path).Name.GetShortName(FatOptions.FileNameEncoding);
+
+ return GetDirectoryEntry(path).Name.ShortName.ToUpperInvariant();
}
public void SetShortName(string path, string name)
@@ -568,7 +568,13 @@ public void SetShortName(string path, string name)
throw new InvalidOperationException("Cannot set short name on root directory");
}
- GetDirectoryEntry(path).Name.SetShortName(name, FatOptions.FileNameEncoding);
+ var id = GetDirectoryEntry(_rootDir, path, out var parent);
+ if (id < 0)
+ {
+ throw new FileNotFoundException("No such file or directory", path);
+ }
+
+ parent.ReplaceShortName(id, name);
}
///
@@ -879,13 +885,7 @@ public override void CopyFile(string sourceFile, string destinationFile, bool ov
{
throw new IOException("The source file is a directory");
}
-
- var newEntry = new DirectoryEntry(sourceEntry)
- {
- Name = FileName.FromPath(destinationFile, FatOptions.FileNameEncoding),
- FirstCluster = 0
- };
-
+
var destEntryId = GetDirectoryEntry(destinationFile, out var destDir);
if (destDir == null)
@@ -893,19 +893,20 @@ public override void CopyFile(string sourceFile, string destinationFile, bool ov
throw new DirectoryNotFoundException($"The destination directory for '{destinationFile}' was not found");
}
+ var resolvedDestinationFile = destinationFile;
+
// If the destination is a directory, use the old file name to construct a full path.
if (destEntryId >= 0)
{
var destEntry = destDir.GetEntry(destEntryId);
if ((destEntry.Attributes & FatAttributes.Directory) != 0)
{
- newEntry.Name = FileName.FromPath(sourceFile, FatOptions.FileNameEncoding);
+ resolvedDestinationFile = sourceFile;
destinationFile = Utilities.CombinePaths(destinationFile, Utilities.GetFileFromPath(sourceFile));
-
destEntryId = GetDirectoryEntry(destinationFile, out destDir);
}
}
-
+
// If there's an existing entry...
if (destEntryId >= 0)
{
@@ -925,6 +926,21 @@ public override void CopyFile(string sourceFile, string destinationFile, bool ov
destDir.DeleteEntry(destEntryId, true);
}
+ FatFileName sourceFileName;
+ try
+ {
+ sourceFileName = FatFileName.NewPath(destDir, resolvedDestinationFile, FatOptions.FileNameEncodingTable);
+ }
+ catch (ArgumentException ex)
+ {
+ throw new IOException($"Failed to create file name '{resolvedDestinationFile}'", ex);
+ }
+
+ var newEntry = new DirectoryEntry(sourceEntry, sourceFileName)
+ {
+ FirstCluster = 0
+ };
+
// Add the new file's entry
destEntryId = destDir.AddEntry(newEntry);
@@ -946,18 +962,16 @@ public override void CreateDirectory(string path)
foreach (var pathElement in pathElements)
{
- FileName name;
+ var pathName = pathElement.ToString();
try
{
- name = new FileName(pathElement.ToString(), FatOptions.FileNameEncoding);
+ var child = focusDir.GetChildDirectory(pathName) ?? focusDir.CreateChildDirectory(pathName);
+ focusDir = child;
}
- catch (ArgumentException ae)
+ catch (ArgumentException ex)
{
- throw new IOException("Invalid path", ae);
+ throw new IOException($"Failed to create directory '{pathName}'", ex);
}
-
- var child = focusDir.GetChildDirectory(name) ?? focusDir.CreateChildDirectory(name);
- focusDir = child;
}
}
@@ -1078,7 +1092,7 @@ public override IEnumerable GetDirectories(string path)
foreach (var dirEntry in entries)
{
- yield return Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding));
+ yield return Utilities.CombinePaths(path, dirEntry.Name.FullName);
}
}
@@ -1110,7 +1124,7 @@ public override IEnumerable GetFiles(string path)
foreach (var dirEntry in entries)
{
- yield return Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding));
+ yield return Utilities.CombinePaths(path, dirEntry.Name.FullName);
}
}
@@ -1142,7 +1156,7 @@ public override IEnumerable GetFileSystemEntries(string path)
foreach (var dirEntry in entries)
{
- yield return Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding));
+ yield return Utilities.CombinePaths(path, dirEntry.Name.FullName);
}
}
@@ -1162,9 +1176,9 @@ public override IEnumerable GetFileSystemEntries(string path, string sea
foreach (var dirEntry in entries)
{
- if (re is null || dirEntry.Name.IsMatch(re, FatOptions.FileNameEncoding))
+ if (re is null || dirEntry.Name.IsMatch(re))
{
- yield return Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding));
+ yield return Utilities.CombinePaths(path, dirEntry.Name.FullName);
}
}
}
@@ -1207,8 +1221,7 @@ public override void MoveDirectory(string sourceDirectoryName, string destinatio
throw new IOException("Source directory doesn't exist");
}
- destParent.AttachChildDirectory(FileName.FromPath(destinationDirectoryName, FatOptions.FileNameEncoding),
- GetDirectory(sourceDirectoryName));
+ destParent.AttachChildDirectory(destinationDirectoryName,GetDirectory(sourceDirectoryName));
sourceParent.DeleteEntry(sourceId, false);
}
@@ -1235,11 +1248,6 @@ public override void MoveFile(string sourceName, string destinationName, bool ov
throw new IOException("The source file is a directory");
}
- var newEntry = new DirectoryEntry(sourceEntry)
- {
- Name = FileName.FromPath(destinationName, FatOptions.FileNameEncoding)
- };
-
var destEntryId = GetDirectoryEntry(destinationName, out var destDir);
if (destDir == null)
@@ -1247,15 +1255,16 @@ public override void MoveFile(string sourceName, string destinationName, bool ov
throw new DirectoryNotFoundException($"The destination directory for '{destinationName}' was not found");
}
+ var resolvedDestinationName = destinationName;
+
// If the destination is a directory, use the old file name to construct a full path.
if (destEntryId >= 0)
{
var destEntry = destDir.GetEntry(destEntryId);
if ((destEntry.Attributes & FatAttributes.Directory) != 0)
{
- newEntry.Name = FileName.FromPath(sourceName, FatOptions.FileNameEncoding);
+ resolvedDestinationName = sourceName;
destinationName = Utilities.CombinePaths(destinationName, Utilities.GetFileFromPath(sourceName));
-
destEntryId = GetDirectoryEntry(destinationName, out destDir);
}
}
@@ -1278,6 +1287,18 @@ public override void MoveFile(string sourceName, string destinationName, bool ov
// Remove the old file
destDir.DeleteEntry(destEntryId, true);
}
+
+ FatFileName sourceFileName;
+ try
+ {
+ sourceFileName = FatFileName.NewPath(destDir, resolvedDestinationName, FatOptions.FileNameEncodingTable);
+ }
+ catch (ArgumentException ex)
+ {
+ throw new IOException($"Failed to create file name '{resolvedDestinationName}'", ex);
+ }
+
+ var newEntry = new DirectoryEntry(sourceEntry, sourceFileName);
// Add the new file's entry and remove the old link to the file's contents
destDir.AddEntry(newEntry);
@@ -1296,7 +1317,6 @@ internal DateTime ConvertFromUtc(DateTime dateTime)
internal Directory GetDirectory(string path)
{
-
if (string.IsNullOrEmpty(path) || path == @"\" || path == "/")
{
return _rootDir;
@@ -1709,14 +1729,19 @@ private void ReadBS(int offset)
}
private long GetDirectoryEntry(Directory dir, string path, out Directory parent)
+ {
+ return GetDirectoryEntry(dir, path, out parent, out _);
+ }
+
+ private long GetDirectoryEntry(Directory dir, string path, out Directory parent, out string lastPathName)
{
var pathElements = path.AsMemory().Split('\\', '/', StringSplitOptions.RemoveEmptyEntries).ToArray();
- return GetDirectoryEntry(dir, pathElements, 0, out parent);
+ return GetDirectoryEntry(dir, pathElements, 0, out parent, out lastPathName);
}
- private long GetDirectoryEntry(Directory dir, ReadOnlyMemory[] pathEntries, int pathOffset, out Directory parent)
+ private long GetDirectoryEntry(Directory dir, ReadOnlyMemory[] pathEntries, int pathOffset, out Directory parent, out string lastPathName)
{
- long entryId;
+ lastPathName = null;
if (pathEntries.Length == 0)
{
@@ -1725,20 +1750,29 @@ private long GetDirectoryEntry(Directory dir, ReadOnlyMemory[] pathEntries
return 0;
}
- entryId = dir.FindEntry(new FileName(pathEntries[pathOffset].ToString(), FatOptions.FileNameEncoding));
- if (entryId >= 0)
+ while (true)
{
- if (pathOffset == pathEntries.Length - 1)
+ long entryId = dir.FindEntry(pathEntries[pathOffset].ToString());
+ if (entryId >= 0)
{
- parent = dir;
- return entryId;
- }
+ if (pathOffset == pathEntries.Length - 1)
+ {
+ parent = dir;
+ return entryId;
+ }
- return GetDirectoryEntry(GetDirectory(dir, entryId), pathEntries, pathOffset + 1, out parent);
+ dir = GetDirectory(dir, entryId);
+ pathOffset++;
+ }
+ else
+ {
+ break;
+ }
}
if (pathOffset == pathEntries.Length - 1)
{
+ lastPathName = pathEntries[pathOffset].ToString();
parent = dir;
return -1;
}
@@ -1760,15 +1794,15 @@ private IEnumerable DoSearch(string path, Func filter, boo
if ((isDir && dirs) || (!isDir && files))
{
- if (filter is null || de.Name.IsMatch(filter, FatOptions.FileNameEncoding))
+ if (filter is null || de.Name.IsMatch(filter))
{
- yield return Utilities.CombinePaths(path, de.Name.GetDisplayName(FatOptions.FileNameEncoding));
+ yield return Utilities.CombinePaths(path, de.Name.FullName);
}
}
if (subFolders && isDir)
{
- foreach (var subdirentry in DoSearch(Utilities.CombinePaths(path, de.Name.GetDisplayName(FatOptions.FileNameEncoding)),
+ foreach (var subdirentry in DoSearch(Utilities.CombinePaths(path, de.Name.FullName),
filter, subFolders, dirs, files))
{
yield return subdirentry;
diff --git a/Library/DiscUtils.Fat/FatFileSystemOptions.cs b/Library/DiscUtils.Fat/FatFileSystemOptions.cs
index 953f1840f..5117ad191 100644
--- a/Library/DiscUtils.Fat/FatFileSystemOptions.cs
+++ b/Library/DiscUtils.Fat/FatFileSystemOptions.cs
@@ -30,38 +30,36 @@ namespace DiscUtils.Fat;
///
public sealed class FatFileSystemOptions : DiscFileSystemOptions
{
- private Encoding _encoding;
-
-#if !NETFRAMEWORK
- static FatFileSystemOptions()
- {
- Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
- }
-#endif
+ private FastEncodingTable _encodingTable;
internal FatFileSystemOptions()
{
- FileNameEncoding = Encoding.GetEncoding(437);
+ _encodingTable = FastEncodingTable.Default;
}
internal FatFileSystemOptions(FileSystemParameters parameters)
{
- if (parameters != null && parameters.FileNameEncoding != null)
+ if (parameters.FileNameEncoding is not null)
{
FileNameEncoding = parameters.FileNameEncoding;
}
else
{
- FileNameEncoding = Encoding.GetEncoding(437);
+ _encodingTable = FastEncodingTable.Default;
}
}
+ ///
+ /// Gets the fast encoding table used for file names.
+ ///
+ internal FastEncodingTable FileNameEncodingTable => _encodingTable;
+
///
/// Gets or sets the character encoding used for file names.
///
public Encoding FileNameEncoding
{
- get => _encoding;
+ get => _encodingTable.Encoding;
set
{
@@ -70,7 +68,7 @@ public Encoding FileNameEncoding
throw new ArgumentException($"{value.EncodingName} is not a single byte encoding");
}
- _encoding = value;
+ _encodingTable = new FastEncodingTable(value);
}
}
}
\ No newline at end of file
diff --git a/Library/DiscUtils.Fat/FileName.cs b/Library/DiscUtils.Fat/FileName.cs
deleted file mode 100644
index 7152658ac..000000000
--- a/Library/DiscUtils.Fat/FileName.cs
+++ /dev/null
@@ -1,433 +0,0 @@
-//
-// Copyright (c) 2008-2011, Kenneth Bell
-//
-// Permission is hereby granted, free of charge, to any person obtaining a
-// copy of this software and associated documentation files (the "Software"),
-// to deal in the Software without restriction, including without limitation
-// the rights to use, copy, modify, merge, publish, distribute, sublicense,
-// and/or sell copies of the Software, and to permit persons to whom the
-// Software is furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in
-// all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-// DEALINGS IN THE SOFTWARE.
-//
-
-using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using DiscUtils.Internal;
-using DiscUtils.Streams;
-using DiscUtils.Streams.Compatibility;
-
-namespace DiscUtils.Fat;
-
-internal sealed class FileName : IEquatable
-{
- private const byte SpaceByte = 0x20;
-
- public static readonly FileName SelfEntryName =
- new(". "u8);
-
- public static readonly FileName ParentEntryName =
- new(".. "u8);
-
- public static readonly FileName Null =
- new("\0\0\0\0\0\0\0\0\0\0\0"u8);
-
- private static readonly byte[] InvalidBytes = "\"*+,./:;<=>?[\\]|"u8.ToArray();
-
- private readonly byte[] _raw;
-
- private readonly string _lfn;
-
- public FileName(ReadOnlySpan data)
- {
- var offset = 0;
-
- // LFN
- // ToDo: This code is not endian-safe. Long file names will be messed
- // up when this code runs on big endian machines.
- if ((data[0] & 0xc0) == 0x40 && data[11] == 0x0f)
- {
- var lfn_entries = data[0] & 0x3f;
-
- Span lfn_chars = stackalloc char[13 * lfn_entries];
-
- lfn_chars.Clear();
-
- var lfn_bytes = MemoryMarshal.AsBytes(lfn_chars);
-
- for (var i = lfn_entries;
- i > 0 && (data[offset] & 0x3f) == i && data[offset + 11] == 0x0f;
- i--, offset += 32)
- {
- data.Slice(offset + 1, 10).CopyTo(lfn_bytes.Slice(26 * (i - 1)));
- data.Slice(offset + 14, 12).CopyTo(lfn_bytes.Slice(26 * (i - 1) + 10));
- data.Slice(offset + 28, 4).CopyTo(lfn_bytes.Slice(26 * (i - 1) + 22));
- }
-
- var nullpos = lfn_chars.IndexOf('\0');
-
- if (nullpos < 0)
- {
- nullpos = lfn_chars.Length;
- }
-
- _lfn = lfn_chars.Slice(0, nullpos).ToString();
- }
-
- _raw = new byte[11];
-
- data.Slice(offset, 11).CopyTo(_raw);
- }
-
- public FileName(string name, Encoding encoding)
- {
-#if NET6_0_OR_GREATER
- ArgumentNullException.ThrowIfNull(name);
-#else
- if (name is null)
- {
- throw new ArgumentNullException(nameof(name));
- }
-#endif
-
- if (name.Length > 255)
- {
- throw new IOException($"Too long name: '{name}'");
- }
-
- _raw = new byte[11];
-
- Span bytes = stackalloc byte[encoding.GetByteCount(name)];
-
- encoding.GetBytes(name, bytes);
-
- if (bytes.Length == 0)
- {
- throw new ArgumentException($"File name too short '{name}'", nameof(name));
- }
-
- var nameIdx = 0;
- var rawIdx = 0;
-
- var extensionPosition = bytes.LastIndexOf((byte)'.');
-
- // If not dot, or last dot is at the beginning, the name
- // is considered to have no extension
- if (extensionPosition <= 0)
- {
- extensionPosition = bytes.Length;
- }
-
-#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
- Span buffer = stackalloc char[4];
-#endif
-
- for (; nameIdx < extensionPosition && rawIdx < 8;nameIdx++)
- {
- var b = bytes[nameIdx];
-
- if (b == '.')
- {
- continue;
- }
-
- if (b == '?')
- {
-#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
- ((int)name[nameIdx]).TryFormat(buffer, out var hexLength, format: "X");
-#else
- var buffer = ((int)name[nameIdx]).ToString("X");
- var hexLength = buffer.Length;
-#endif
-
- for (var i = 0; i < hexLength && rawIdx < 8; i++)
- {
- _raw[rawIdx++] = (byte)buffer[i];
- }
-
- continue;
- }
-
- if (b < 0x20 || Contains(InvalidBytes, b))
- {
- throw new ArgumentException($"Invalid character in file name '{(char)b}'", nameof(name));
- }
-
- _raw[rawIdx++] = (byte)char.ToUpperInvariant((char)b);
- }
-
- if (rawIdx > 8)
- {
- //throw new ArgumentException($"File name too long '{name}'", nameof(name));
- }
-
- if (rawIdx == 0)
- {
- //throw new ArgumentException($"File name too short '{name}'", nameof(name));
- }
-
- while (rawIdx < 8)
- {
- _raw[rawIdx++] = SpaceByte;
- }
-
- if (nameIdx < bytes.Length && bytes[nameIdx] == '.')
- {
- ++nameIdx;
- }
-
- for (; nameIdx < bytes.Length && rawIdx < _raw.Length; nameIdx++)
- {
- var b = bytes[nameIdx];
-
- if (b == '?')
- {
-#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
- ((int)name[nameIdx]).TryFormat(buffer, out var hexLength, format: "X");
-#else
- var buffer = ((int)name[nameIdx]).ToString("X");
- var hexLength = buffer.Length;
-#endif
-
- for (var i = 0; i < hexLength && rawIdx < _raw.Length; i++)
- {
- _raw[rawIdx++] = (byte)buffer[i];
- }
-
- continue;
- }
-
- if (b < 0x20 || Contains(InvalidBytes, b))
- {
- throw new ArgumentException($"Invalid character in file extension '{(char)b}'", nameof(name));
- }
-
- _raw[rawIdx++] = b;
- }
-
- while (rawIdx < 11)
- {
- _raw[rawIdx++] = SpaceByte;
- }
-
- if (nameIdx != bytes.Length)
- {
- //throw new ArgumentException($"File extension too long '{name}'", nameof(name));
- }
-
- _lfn = name;
- }
-
- public bool Equals(FileName other) => Equals(this, other);
-
- public static bool Equals(FileName a, FileName b)
- {
- if (ReferenceEquals(a, b))
- {
- return true;
- }
-
- if (a is null || b is null)
- {
- return false;
- }
-
- if (a._lfn != null)
- {
- if (StringComparer.OrdinalIgnoreCase.Equals(a._lfn, b._lfn) ||
- StringComparer.OrdinalIgnoreCase.Equals(a._lfn, b.GetShortName(Encoding.ASCII)))
- {
- return true;
- }
- }
- else if (b._lfn != null)
- {
- if (StringComparer.OrdinalIgnoreCase.Equals(a.GetShortName(Encoding.ASCII), b._lfn))
- {
- return true;
- }
- }
- else
- {
- if (CompareRawNames(a, b) == 0)
- {
- return true;
- }
- }
-
- return false;
- }
-
- public static FileName FromPath(string path, Encoding encoding)
- {
- return new FileName(Utilities.GetFileFromPath(path), encoding);
- }
-
- public static bool operator ==(FileName a, FileName b) => Equals(a, b);
-
- public static bool operator !=(FileName a, FileName b) => !Equals(a, b);
-
- public string GetDisplayName(Encoding encoding)
- {
- return _lfn ?? GetShortName(encoding);
- }
-
- public string GetShortName(Encoding encoding)
- {
- return $"{encoding.GetString(_raw, 0, 8).TrimEnd()}.{encoding.GetString(_raw, 8, 3)}".TrimEnd('.', ' ');
- }
-
- public void SetShortName(string name, Encoding encoding)
- {
- var bytes = encoding.GetBytes(name.ToUpperInvariant());
-
- var nameIdx = 0;
- var rawIdx = 0;
-
- while (nameIdx < bytes.Length && bytes[nameIdx] != '.' && rawIdx < _raw.Length)
- {
- var b = bytes[nameIdx++];
- if (b < 0x20 || Contains(InvalidBytes, b))
- {
- throw new ArgumentException($"Invalid character in file name '{(char)b}'", nameof(name));
- }
-
- _raw[rawIdx++] = b;
- }
-
- if (rawIdx > 8)
- {
- throw new ArgumentException($"File name too long '{name}'", nameof(name));
- }
-
- if (rawIdx == 0)
- {
- throw new ArgumentException($"File name too short '{name}'", nameof(name));
- }
-
- while (rawIdx < 8)
- {
- _raw[rawIdx++] = SpaceByte;
- }
-
- if (nameIdx < bytes.Length && bytes[nameIdx] == '.')
- {
- ++nameIdx;
- }
-
- while (nameIdx < bytes.Length && rawIdx < _raw.Length)
- {
- var b = bytes[nameIdx++];
- if (b < 0x20 || Contains(InvalidBytes, b))
- {
- throw new ArgumentException($"Invalid character in file extension '{(char)b}'", nameof(name));
- }
-
- _raw[rawIdx++] = b;
- }
-
- while (rawIdx < 11)
- {
- _raw[rawIdx++] = SpaceByte;
- }
-
- if (nameIdx != bytes.Length)
- {
- throw new ArgumentException($"File extension too long '{name}'", nameof(name));
- }
- }
-
- public bool IsMatch(Func filter, Encoding encoding)
- {
- var search_name = GetDisplayName(encoding);
- if (search_name.IndexOf('.') < 0)
- {
- search_name += '.';
- }
-
- return filter(search_name);
- }
-
- public string GetRawName(Encoding encoding) => encoding.GetString(_raw, 0, 11).TrimEnd();
-
- public FileName Deleted()
- {
- Span data = stackalloc byte[11];
- _raw.AsSpan(0, 11).CopyTo(data);
- data[0] = 0xE5;
-
- return new FileName(data);
- }
-
- public bool IsDeleted()
- {
- return _raw[0] == 0xE5;
- }
-
- public bool IsEndMarker()
- {
- return _raw[0] == 0x00;
- }
-
- public void GetBytes(Span data)
- {
- _raw.AsSpan(0, 11).CopyTo(data);
- }
-
- public override bool Equals(object other)
- => other is FileName otherName && Equals(this, otherName);
-
- public override int GetHashCode()
- {
- if (_lfn != null)
- {
- return _lfn.GetHashCode();
- }
-
- var val = new HashCode();
-
- for (var i = 0; i < 11; ++i)
- {
- val.Add(_raw[i]);
- }
-
- return val.ToHashCode();
- }
-
- private static int CompareRawNames(FileName a, FileName b)
- {
- if (a._lfn != null || b._lfn != null)
- {
- return StringComparer.OrdinalIgnoreCase.Compare(a.ToString(), b.ToString());
- }
-
- for (var i = 0; i < 11; ++i)
- {
- if (a._raw[i] != b._raw[i])
- {
- return a._raw[i] - b._raw[i];
- }
- }
-
- return 0;
- }
-
- private static bool Contains(byte[] array, byte val) => Array.IndexOf(array, val) >= 0;
-
- public string Lfn => _lfn;
-
- public string ShortName => GetShortName(Encoding.ASCII);
-
- public override string ToString() => GetDisplayName(Encoding.ASCII);
-}
\ No newline at end of file
diff --git a/Library/DiscUtils.Fat/FreeDirectoryEntryTable.cs b/Library/DiscUtils.Fat/FreeDirectoryEntryTable.cs
new file mode 100644
index 000000000..a02352f17
--- /dev/null
+++ b/Library/DiscUtils.Fat/FreeDirectoryEntryTable.cs
@@ -0,0 +1,153 @@
+//
+// Copyright (c) 2024, Olof Lagerkvist and contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Numerics;
+
+namespace DiscUtils.Fat;
+
+///
+/// Free directory entry table to manage free directory entries.
+///
+///
+/// NOTE: This allocator could be optimized further to share/recycle allocations of SortedSet<long> instances through the .
+///
+internal struct FreeDirectoryEntryTable
+{
+ private const int MaxBucketCount = 32;
+
+ private uint _bucketMask;
+ private readonly SortedSet[] _buckets;
+ private readonly Stack> _freeList;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public FreeDirectoryEntryTable()
+ {
+ _buckets = new SortedSet[MaxBucketCount];
+ _freeList = new();
+ }
+
+ ///
+ /// Add a free-range of directory entries.
+ ///
+ /// The first position to be free
+ /// The number of directory entries free after the position.
+ public void AddFreeRange(long position, int count)
+ {
+ if (count <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count), "The number of directory entries to add must be greater than zero.");
+ }
+
+ while (count > 0)
+ {
+ var bucket = Math.Min(count, MaxBucketCount);
+ AddInternal(position, bucket);
+ count -= bucket;
+ position += bucket * DirectoryEntry.SizeOf;
+ }
+ }
+
+ ///
+ /// Allocate a free directory entry.
+ ///
+ /// The number of directory entries to allocate.
+ /// The position of the first directory entry allocated or -1 if no free directory entries are available.
+ public long Allocate(int originalCount)
+ {
+ if (originalCount <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(originalCount), "The number of directory entries to allocate must be greater than zero.");
+ }
+
+ if (originalCount > MaxBucketCount)
+ {
+ throw new ArgumentOutOfRangeException(nameof(originalCount), "The number of directory entries to allocate is too large.");
+ }
+ var bucketIndex = originalCount - 1;
+ var bucketMask = _bucketMask >>> bucketIndex;
+ // If there are no buckets or the requested count is larger than the largest bucket, return -1
+ if (bucketMask == 0)
+ {
+ return -1;
+ }
+
+ // Find the first bucket with free space
+ var offset = TrailingZeroCount(bucketMask);
+
+ // Resolve the bucket index
+ var resolvedBucketIndex = bucketIndex + offset;
+
+ // Get the first position in the bucket
+ ref var bucket = ref _buckets[resolvedBucketIndex];
+ // There is a guarantee that the bucket is not null since we have a mask bit set
+ var pos = bucket.Min;
+ // We can remove the position from the bucket
+ bucket.Remove(pos);
+
+ // If we have a larger bucket, split it and add the remaining to the free list
+ if (offset > 0)
+ {
+ // offset becomes the number of remaining entries that were not used by the allocation
+ AddFreeRange(pos + originalCount * DirectoryEntry.SizeOf, offset);
+ }
+
+ // If the bucket is empty, remove it
+ if (bucket.Count == 0)
+ {
+ _bucketMask &= ~(1U << resolvedBucketIndex);
+ _freeList.Push(bucket);
+ bucket = null;
+ }
+
+ return pos;
+ }
+
+ private void AddInternal(long position, int bucketIndex)
+ {
+ Debug.Assert(bucketIndex > 0);
+ bucketIndex--;
+ ref var bucket = ref _buckets[bucketIndex];
+ bucket ??= _freeList.Count > 0 ? _freeList.Pop() : new();
+ bucket.Add(position);
+ _bucketMask |= 1U << bucketIndex;
+ }
+
+ private static int TrailingZeroCount(uint value)
+ {
+#if NET6_0_OR_GREATER
+ return BitOperations.TrailingZeroCount(value);
+#else
+ // This could be more optimized but this is for old .NET framework versions
+ var count = 0;
+ while ((value & 1) == 0)
+ {
+ count++;
+ value >>>= 1;
+ }
+
+ return count;
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Tests/LibraryTests/Fat/FatFileNameTest.cs b/Tests/LibraryTests/Fat/FatFileNameTest.cs
new file mode 100644
index 000000000..97e2219d6
--- /dev/null
+++ b/Tests/LibraryTests/Fat/FatFileNameTest.cs
@@ -0,0 +1,99 @@
+//
+// Copyright (c) 2024, Olof Lagerkvist and contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+using System;
+using DiscUtils.Fat;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace LibraryTests.Fat;
+
+public class FatFileNameTest
+{
+
+ private readonly ITestOutputHelper _output;
+ public FatFileNameTest(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ // Generate short name from long name Xunit tests cases
+ [Theory]
+ [InlineData("A", "A", false)]
+ [InlineData("A.B", "A.B", false)]
+ [InlineData("a", "a", false)]
+ [InlineData("a.b", "a.b", false)]
+ [InlineData("a.B", "a.B", false)]
+ [InlineData("A1234567", "A1234567", false)]
+ [InlineData("A1234567.ext", "A1234567.ext", false)]
+ [InlineData("this_is_a_long_name", "THIS_I~1", true)]
+ [InlineData("V1Abcd_this_is_to_long.TXT", "V1ABCD~2.TXT", true)]
+ [InlineData("V2Abcd_this_is_to_long.TXT", "V2DB58~1.TXT", true)]
+ [InlineData("✨.txt", "6393~1.TXT", true)]
+ [InlineData("abcdef🙂.txt", "ABCDEF~1.TXT", true)]
+ [InlineData("abc🙂.txt", "ABC~1.TXT", true)]
+ [InlineData("ab🙂.txt", "AB1F60~1.TXT", true)]
+ [InlineData("c d.txt", "CD2C67~1.TXT", true)]
+ [InlineData("...txt", "TXT~1", true)]
+ [InlineData("..a.txt", "A2632~1.TXT", true)]
+ [InlineData("txt...", "txt", false)]
+ [InlineData("a+b=c", "A_B_C~1", true)]
+ [InlineData("ab .txt", "AB1775~1.TXT", true)]
+ [InlineData("✨TAT", "TAT~1", true)]
+ [InlineData("a.b..c.d", "ABC~1.D", true)]
+ [InlineData("Mixed.Cas", "MIXED.CAS", true)]
+ [InlineData("Mixed.txt", "MIXED.TXT", true)]
+ [InlineData("mixed.Txt", "MIXED.TXT", true)]
+ public void TestShortName(string name, string expectedShortName, bool expectedLongName)
+ {
+ Func resolver = name.StartsWith("V1") ? IsShortNameExits1 : name.StartsWith("V2") ? IsShortNameExists2 : shortName => false;
+ var fileName = FatFileName.FromName(name, FastEncodingTable.Default, resolver);
+ Assert.Equal(expectedShortName, fileName.ShortName);
+ Assert.Equal(expectedLongName, fileName.LongName is not null);
+ if (expectedLongName)
+ {
+ Assert.Equal(name, fileName.LongName);
+ }
+
+ var size = (1 + fileName.LfnDirectoryEntryCount) * DirectoryEntry.SizeOf;
+ var buffer = new byte[size];
+ fileName.ToDirectoryEntryBytes(buffer, FastEncodingTable.Default);
+
+ var fileName2 = FatFileName.FromDirectoryEntryBytes(buffer, FastEncodingTable.Default, out int offset);
+
+ Assert.Equal(size, offset);
+ Assert.Equal(fileName.ShortName, fileName2.ShortName);
+ Assert.Equal(fileName.LongName, fileName2.LongName);
+
+
+ static bool IsShortNameExits1(string name)
+ {
+ return name.Equals("V1ABCD~1.TXT", StringComparison.OrdinalIgnoreCase);
+ }
+
+ static bool IsShortNameExists2(string name)
+ {
+ return name.Equals("V2ABCD~1.TXT", StringComparison.OrdinalIgnoreCase)
+ || name.Equals("V2ABCD~2.TXT", StringComparison.OrdinalIgnoreCase)
+ || name.Equals("V2ABCD~3.TXT", StringComparison.OrdinalIgnoreCase)
+ || name.Equals("V2ABCD~4.TXT", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/LibraryTests/Fat/FatFileSystemTest.cs b/Tests/LibraryTests/Fat/FatFileSystemTest.cs
index c3d5fe3b9..6378386ae 100644
--- a/Tests/LibraryTests/Fat/FatFileSystemTest.cs
+++ b/Tests/LibraryTests/Fat/FatFileSystemTest.cs
@@ -41,7 +41,7 @@ public void FormatFloppy()
var fs = FatFileSystem.FormatFloppy(ms, FloppyDiskType.HighDensity, "KBFLOPPY ");
}
- [Fact(Skip = "Saving LFN not yet implemented")]
+ [Fact]
public void Cyrillic()
{
SetupHelper.RegisterAssembly(typeof(FatFileSystem).Assembly);
@@ -240,4 +240,222 @@ public void InvalidImageThrowsException()
stream.Position = 0;
Assert.Throws(() => new FatFileSystem(stream));
}
+
+ [Fact]
+ public void TestShortNameDeletedEntries()
+ {
+ var diskStream = new SparseMemoryStream();
+ {
+ using var fs = FatFileSystem.FormatFloppy(diskStream, FloppyDiskType.HighDensity, "FLOPPY_IMG ");
+
+ fs.CreateDirectory(@"FOO1");
+ fs.CreateDirectory(@"FOO2");
+ fs.CreateDirectory(@"FOO3");
+ fs.CreateDirectory(@"FOO4");
+ fs.CreateDirectory(@"BAR");
+ fs.CreateDirectory(@"BAR1");
+ fs.CreateDirectory(@"BAR2");
+ fs.CreateDirectory(@"BAR3");
+
+ fs.DeleteDirectory(@"FOO1");
+ fs.DeleteDirectory(@"FOO2");
+ fs.DeleteDirectory(@"FOO3");
+ fs.DeleteDirectory(@"FOO4");
+ fs.DeleteDirectory(@"BAR1");
+ fs.DeleteDirectory(@"BAR2");
+ fs.DeleteDirectory(@"BAR3");
+ fs.CreateDirectory(@"01234567890123456789.txt");
+ }
+
+ {
+ var fs = new FatFileSystem(diskStream);
+ var entries = fs.GetFileSystemEntries("\\").OrderBy(x => x).ToList();
+ Assert.Equal(2, entries.Count);
+ Assert.Equal("\\01234567890123456789.txt", entries[0]);
+ Assert.Equal("\\BAR", entries[1]);
+
+ fs.CreateDirectory("abcdefghijklmnop.txt");
+
+ entries = fs.GetFileSystemEntries("\\").OrderBy(x => x).ToList();
+ Assert.Equal(3, entries.Count);
+ Assert.Equal("\\01234567890123456789.txt", entries[0]);
+ Assert.Equal("\\abcdefghijklmnop.txt", entries[1]);
+ Assert.Equal("\\BAR", entries[2]);
+ }
+ }
+
+ [Fact]
+ public void TestLongNameDeletedEntries()
+ {
+ var diskStream = new SparseMemoryStream();
+ {
+ using var fs = FatFileSystem.FormatFloppy(diskStream, FloppyDiskType.HighDensity, "FLOPPY_IMG ");
+
+ fs.CreateDirectory(@"FOO_This_is_a_long_entry_1");
+ fs.CreateDirectory(@"FOO_This_is_a_long_entry_2");
+ fs.CreateDirectory(@"FOO_This_is_a_long_entry_3");
+ fs.CreateDirectory(@"FOO_This_is_a_long_entry_4");
+
+ fs.DeleteDirectory(@"FOO_This_is_a_long_entry_1"); // 26 characters, should take 3 entries (2 for LFN + 1 for SFN)
+ fs.CreateDirectory("TA"); // Should take the entry of the previously deleted entry
+ fs.CreateDirectory("TB");
+ fs.CreateDirectory("TC");
+ }
+
+ {
+ var fs = new FatFileSystem(diskStream);
+ var entries = fs.GetFileSystemEntries("\\").OrderBy(x => x).ToList();
+ Assert.Equal(6, entries.Count);
+ Assert.Equal("\\FOO_This_is_a_long_entry_2", entries[0]);
+ Assert.Equal("\\FOO_This_is_a_long_entry_3", entries[1]);
+ Assert.Equal("\\FOO_This_is_a_long_entry_4", entries[2]);
+ Assert.Equal("\\TA", entries[3]);
+ Assert.Equal("\\TB", entries[4]);
+ Assert.Equal("\\TC", entries[5]);
+ }
+ }
+
+ [Fact]
+ public void TestCreateDirectoryAndFailure()
+ {
+ var diskStream = new SparseMemoryStream();
+ {
+ using var fs = FatFileSystem.FormatFloppy(diskStream, FloppyDiskType.HighDensity, "FLOPPY_IMG ");
+
+ fs.CreateDirectory(@"BAR\BAZ\QUX");
+ fs.CreateDirectory(@"BAR\BAZ\QUX"); // Nothing is happening here
+ fs.CreateDirectory(@"BAR");
+ {
+ using var file = fs.OpenFile(@"BAR\BAZ\QUX\TEST", FileMode.Create);
+ file.WriteByte(0);
+ }
+
+ Assert.Throws(() => fs.CreateDirectory(@"BAR\BAZ\QUX\TEST"));
+ }
+ }
+
+ [Fact]
+ public void TestLargeFileCreateOpenAppendTruncate()
+ {
+ var diskStream = new SparseMemoryStream();
+ {
+ using var fs = FatFileSystem.FormatFloppy(diskStream, FloppyDiskType.HighDensity, "FLOPPY_IMG ");
+
+ var buffer = new byte[1024 * 1024];
+ var rnd = new Random(0);
+ rnd.NextBytes(buffer);
+ using (var file = fs.OpenFile("TEST", FileMode.Create))
+ {
+ file.Write(buffer, 0, buffer.Length);
+ }
+
+ using (var file = fs.OpenFile("TEST", FileMode.Open))
+ {
+ var buffer2 = new byte[buffer.Length];
+ int length = file.Read(buffer2, 0, buffer2.Length);
+ Assert.Equal(length, buffer2.Length);
+
+ for (int i = 0; i < buffer.Length; i++)
+ {
+ Assert.Equal(buffer[i], buffer2[i]);
+ }
+ }
+
+ using (var file = fs.OpenFile("TEST", FileMode.Append))
+ {
+ var smallerBuffer = new byte[] { 1, 2, 3, 4 };
+ file.Write(smallerBuffer, 0, smallerBuffer.Length);
+ }
+
+ using (var file = fs.OpenFile("TEST", FileMode.Open))
+ {
+ var buffer2 = new byte[buffer.Length + 4];
+ int length = file.Read(buffer2, 0, buffer2.Length);
+ Assert.Equal(length, buffer2.Length);
+
+ for (int i = 0; i < buffer.Length; i++)
+ {
+ Assert.Equal(buffer[i], buffer2[i]);
+ }
+
+ for (int i = 0; i < 4; i++)
+ {
+ Assert.Equal(i + 1, buffer2[buffer.Length + i]);
+ }
+ }
+
+ using (var file = fs.OpenFile("TEST", FileMode.Truncate))
+ {
+ file.Write([0]);
+ }
+
+ var attr = fs.GetFileLength("TEST");
+ Assert.Equal(1, attr);
+
+ fs.DeleteFile("TEST");
+
+ Assert.Throws(() => fs.GetFileLength("TEST"));
+
+ using (var file = fs.OpenFile("ANOTHER", FileMode.Create))
+ {
+ file.Write(buffer, 0, buffer.Length);
+ }
+
+ Assert.True(fs.FileExists("ANOTHER"));
+ }
+ }
+
+ [Fact]
+ public void TestShortName()
+ {
+ var diskStream = new SparseMemoryStream();
+ {
+ using var fs = FatFileSystem.FormatFloppy(diskStream, FloppyDiskType.HighDensity, "FLOPPY_IMG ");
+
+ fs.CreateDirectory("A");
+ fs.CreateDirectory("A.B");
+ fs.CreateDirectory("a");
+ fs.CreateDirectory("a.b");
+ fs.CreateDirectory("a.B");
+ fs.CreateDirectory("A1234567");
+ fs.CreateDirectory("A1234567.ext");
+ fs.CreateDirectory("this_is_a_long_name");
+ fs.CreateDirectory("V1Abcd_this_is_to_long.TXT");
+ fs.CreateDirectory("V2Abcd_this_is_to_long.TXT");
+ fs.CreateDirectory("✨.txt");
+ fs.CreateDirectory("abcdef🙂.txt");
+ fs.CreateDirectory("abc🙂.txt");
+ fs.CreateDirectory("ab🙂.txt");
+ fs.CreateDirectory("c d.txt");
+ fs.CreateDirectory("...txt");
+ fs.CreateDirectory("..a.txt");
+ fs.CreateDirectory("txt...");
+ fs.CreateDirectory("a+b=c");
+ fs.CreateDirectory("ab .txt");
+ fs.CreateDirectory("✨TAT");
+ fs.CreateDirectory("a.b..c.d");
+ fs.CreateDirectory("Mixed.Cas");
+ fs.CreateDirectory("Mixed.txt");
+ fs.CreateDirectory("mixed.Txt");
+
+ Assert.Equal("A", fs.GetShortName("A"));
+ Assert.Equal("A.B", fs.GetShortName("A.B"));
+ Assert.Equal("A1234567", fs.GetShortName("A1234567"));
+ Assert.Equal("A1234567.EXT", fs.GetShortName("A1234567.ext"));
+ Assert.Equal("THIS_I~1", fs.GetShortName("this_is_a_long_name"));
+ Assert.Equal("V1ABCD~1.TXT", fs.GetShortName("V1Abcd_this_is_to_long.TXT"));
+ Assert.Equal("V2ABCD~1.TXT", fs.GetShortName("V2Abcd_this_is_to_long.TXT"));
+ Assert.Equal("6393~1.TXT", fs.GetShortName("✨.txt"));
+ Assert.Equal("ABCDEF~1.TXT", fs.GetShortName("abcdef🙂.txt"));
+ Assert.Equal("ABC~1.TXT", fs.GetShortName("abc🙂.txt"));
+ Assert.Equal("AB1F60~1.TXT", fs.GetShortName("ab🙂.txt"));
+
+ // Force changing the short name
+ fs.SetShortName("abcdef🙂.txt", "HELLO.TXT");
+ Assert.Equal("HELLO.TXT", fs.GetShortName("abcdef🙂.txt"));
+
+ // This should not be possible because the entry HELLO.TXT already exists
+ Assert.Throws(() => fs.SetShortName("abc🙂.txt", "HELLO.TXT"));
+ }
+ }
}
diff --git a/Tests/LibraryTests/Fat/FreeDirectoryEntryTableTest.cs b/Tests/LibraryTests/Fat/FreeDirectoryEntryTableTest.cs
new file mode 100644
index 000000000..5d3c8833b
--- /dev/null
+++ b/Tests/LibraryTests/Fat/FreeDirectoryEntryTableTest.cs
@@ -0,0 +1,106 @@
+//
+// Copyright (c) 2024, Olof Lagerkvist and contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using DiscUtils.Fat;
+using Xunit;
+
+namespace LibraryTests.Fat;
+
+///
+/// Tests for .
+///
+public class FreeDirectoryEntryTableTest
+{
+ [Fact]
+ public void TestSimple()
+ {
+ var freeTable = new FreeDirectoryEntryTable();
+
+ // Allocation should fail, we don't have any range free
+ Assert.Equal(-1, freeTable.Allocate(1));
+
+ // Allocate 10 entries
+ freeTable.AddFreeRange(0, 10);
+ Assert.Equal(0, freeTable.Allocate(10));
+ Assert.Equal(-1, freeTable.Allocate(1));
+
+ // Allocate 2 x 5 entries
+ freeTable.AddFreeRange(0, 10);
+ Assert.Equal(0, freeTable.Allocate(5));
+ Assert.Equal(5 * DirectoryEntry.SizeOf, freeTable.Allocate(5));
+ Assert.Equal(-1, freeTable.Allocate(1));
+
+ // Allocate 20 = 10 + 5 + 4 + 1 entries
+ freeTable.AddFreeRange(0, 20);
+ Assert.Equal(0, freeTable.Allocate(10));
+ Assert.Equal(10 * DirectoryEntry.SizeOf, freeTable.Allocate(5));
+ Assert.Equal(15 * DirectoryEntry.SizeOf, freeTable.Allocate(4));
+ Assert.Equal(19 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(-1, freeTable.Allocate(1));
+
+ // Add 50 free range
+ // 32 + 18
+ freeTable.AddFreeRange(0, 50);
+ // The free table allows to allocate 32 entries, so the first entry available will be at offset 32
+ // 32 + 18 => takes on entry 18
+ // 32 + 8 left
+ Assert.Equal(32 * DirectoryEntry.SizeOf, freeTable.Allocate(10));
+ // Cannot allocate on 8, so allocate on 32
+ // 22 + 8 left
+ Assert.Equal(0 * DirectoryEntry.SizeOf, freeTable.Allocate(10));
+ // 12 + 8 left
+ Assert.Equal(10 * DirectoryEntry.SizeOf, freeTable.Allocate(10));
+ // 2 + 8 left
+ Assert.Equal(20 * DirectoryEntry.SizeOf, freeTable.Allocate(10));
+ // Cannot allocate 10 consecutive entries
+ Assert.Equal(-1, freeTable.Allocate(10));
+
+ // 2 entries consecutive left
+ Assert.Equal(30 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(31 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+
+ // 8 entries consecutive left
+ Assert.Equal(42 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(43 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(44 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(45 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(46 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(47 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(48 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+ Assert.Equal(49 * DirectoryEntry.SizeOf, freeTable.Allocate(1));
+
+ // We should not be able to allocate from there
+ Assert.Equal(-1, freeTable.Allocate(1));
+ }
+
+ [Fact]
+ public void TestInvalidValues()
+ {
+ var freeTable = new FreeDirectoryEntryTable();
+
+ Assert.Throws(() => freeTable.AddFreeRange(0, 0));
+ Assert.Throws(() => freeTable.AddFreeRange(0, -1));
+
+ Assert.Throws(() => freeTable.Allocate(0));
+ Assert.Throws(() => freeTable.Allocate(50));
+ }
+}
\ No newline at end of file
diff --git a/Tests/LibraryTests/LibraryTests.csproj b/Tests/LibraryTests/LibraryTests.csproj
index ea913913b..f3e7fa44f 100644
--- a/Tests/LibraryTests/LibraryTests.csproj
+++ b/Tests/LibraryTests/LibraryTests.csproj
@@ -26,6 +26,7 @@
+
@@ -38,6 +39,7 @@
+