diff --git a/src/LibHac/Cnmt.cs b/src/LibHac/Cnmt.cs index 9b0d375e..b5616af3 100644 --- a/src/LibHac/Cnmt.cs +++ b/src/LibHac/Cnmt.cs @@ -1,6 +1,8 @@ using System.IO; using System.Linq; -using LibHac.Fs.NcaUtils; +using LibHac.FsSystem.NcaUtils; +using LibHac.Ncm; +using ContentType = LibHac.Ncm.ContentType; namespace LibHac { @@ -8,7 +10,7 @@ namespace LibHac { public ulong TitleId { get; } public TitleVersion TitleVersion { get; } - public TitleType Type { get; } + public ContentMetaType Type { get; } public byte FieldD { get; } public int TableOffset { get; } public int ContentEntryCount { get; } @@ -34,8 +36,8 @@ namespace LibHac { TitleId = reader.ReadUInt64(); uint version = reader.ReadUInt32(); - Type = (TitleType)reader.ReadByte(); - TitleVersion = new TitleVersion(version, Type < TitleType.Application); + Type = (ContentMetaType)reader.ReadByte(); + TitleVersion = new TitleVersion(version, Type < ContentMetaType.Application); FieldD = reader.ReadByte(); TableOffset = reader.ReadUInt16(); ContentEntryCount = reader.ReadUInt16(); @@ -44,16 +46,16 @@ namespace LibHac switch (Type) { - case TitleType.Application: + case ContentMetaType.Application: ApplicationTitleId = TitleId; PatchTitleId = reader.ReadUInt64(); MinimumSystemVersion = new TitleVersion(reader.ReadUInt32(), true); break; - case TitleType.Patch: + case ContentMetaType.Patch: ApplicationTitleId = reader.ReadUInt64(); MinimumSystemVersion = new TitleVersion(reader.ReadUInt32(), true); break; - case TitleType.AddOnContent: + case ContentMetaType.AddOnContent: ApplicationTitleId = reader.ReadUInt64(); MinimumApplicationVersion = new TitleVersion(reader.ReadUInt32()); break; @@ -74,7 +76,7 @@ namespace LibHac MetaEntries[i] = new CnmtContentMetaEntry(reader); } - if (Type == TitleType.Patch) + if (Type == ContentMetaType.Patch) { ExtendedData = new CnmtExtended(reader); } @@ -89,7 +91,7 @@ namespace LibHac public byte[] Hash { get; set; } public byte[] NcaId { get; set; } public long Size { get; set; } - public CnmtContentType Type { get; set; } + public ContentType Type { get; set; } public CnmtContentEntry() { } @@ -99,7 +101,7 @@ namespace LibHac NcaId = reader.ReadBytes(0x10); Size = reader.ReadUInt32(); Size |= (long)reader.ReadUInt16() << 32; - Type = (CnmtContentType)reader.ReadByte(); + Type = (ContentType)reader.ReadByte(); reader.BaseStream.Position += 1; } } @@ -108,7 +110,7 @@ namespace LibHac { public ulong TitleId { get; } public TitleVersion Version { get; } - public CnmtContentType Type { get; } + public ContentType Type { get; } public CnmtContentMetaEntry() { } @@ -116,7 +118,7 @@ namespace LibHac { TitleId = reader.ReadUInt64(); Version = new TitleVersion(reader.ReadUInt32(), true); - Type = (CnmtContentType)reader.ReadByte(); + Type = (ContentType)reader.ReadByte(); reader.BaseStream.Position += 3; } } @@ -199,7 +201,7 @@ namespace LibHac { public ulong TitleId { get; } public TitleVersion Version { get; } - public TitleType Type { get; } + public ContentMetaType Type { get; } public byte[] Hash { get; } public short ContentCount { get; } public short CnmtPrevMetaEntryField32 { get; } @@ -209,7 +211,7 @@ namespace LibHac { TitleId = reader.ReadUInt64(); Version = new TitleVersion(reader.ReadUInt32()); - Type = (TitleType)reader.ReadByte(); + Type = (ContentMetaType)reader.ReadByte(); reader.BaseStream.Position += 3; Hash = reader.ReadBytes(0x20); ContentCount = reader.ReadInt16(); @@ -265,8 +267,8 @@ namespace LibHac public long SizeOld { get; } public long SizeNew { get; } public short FragmentCount { get; } - public CnmtContentType Type { get; } - public CnmtDeltaType DeltaType { get; } + public ContentType Type { get; } + public UpdateType DeltaType { get; } public int FragmentSetInfoField30 { get; } @@ -281,8 +283,8 @@ namespace LibHac SizeNew = reader.ReadUInt32(); FragmentCount = reader.ReadInt16(); - Type = (CnmtContentType)reader.ReadByte(); - DeltaType = (CnmtDeltaType)reader.ReadByte(); + Type = (ContentType)reader.ReadByte(); + DeltaType = (UpdateType)reader.ReadByte(); FragmentSetInfoField30 = reader.ReadInt32(); } } @@ -291,14 +293,14 @@ namespace LibHac { public byte[] NcaId { get; } public long Size { get; } - public CnmtContentType Type { get; } + public ContentType Type { get; } public CnmtPrevContent(BinaryReader reader) { NcaId = reader.ReadBytes(0x10); Size = reader.ReadUInt32(); Size |= (long)reader.ReadUInt16() << 32; - Type = (CnmtContentType)reader.ReadByte(); + Type = (ContentType)reader.ReadByte(); reader.BaseStream.Position += 1; } } @@ -314,35 +316,4 @@ namespace LibHac FragmentIndex = reader.ReadInt16(); } } - - public enum CnmtContentType - { - Meta, - Program, - Data, - Control, - HtmlDocument, - LegalInformation, - DeltaFragment - } - - public enum CnmtDeltaType - { - Delta, - Replace, - NewContent - } - - public enum TitleType - { - SystemProgram = 1, - SystemData, - SystemUpdate, - BootImagePackage, - BootImagePackageSafe, - Application = 0x80, - Patch, - AddOnContent, - Delta - } } diff --git a/src/LibHac/Common/BlitStruct.cs b/src/LibHac/Common/BlitStruct.cs new file mode 100644 index 00000000..70e4dad8 --- /dev/null +++ b/src/LibHac/Common/BlitStruct.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibHac.Common +{ + public ref struct BlitStruct where T : unmanaged + { + private readonly Span _buffer; + + public int Length => _buffer.Length; + + public ref T Value => ref _buffer[0]; + public ref T this[int index] => ref _buffer[index]; + + public BlitStruct(Span data) + { + _buffer = data; + + Debug.Assert(_buffer.Length != 0); + } + + public BlitStruct(Span data) + { + _buffer = MemoryMarshal.Cast(data); + + Debug.Assert(_buffer.Length != 0); + } + + public BlitStruct(ref T data) + { + _buffer = SpanHelpers.AsSpan(ref data); + } + + public Span GetByteSpan() + { + return MemoryMarshal.Cast(_buffer); + } + + public Span GetByteSpan(int elementIndex) + { + Span element = _buffer.Slice(elementIndex, 1); + return MemoryMarshal.Cast(element); + } + + public static int QueryByteLength(int elementCount) + { + return Unsafe.SizeOf() * elementCount; + } + } +} diff --git a/src/LibHac/Common/HResult.cs b/src/LibHac/Common/HResult.cs new file mode 100644 index 00000000..728c6e21 --- /dev/null +++ b/src/LibHac/Common/HResult.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; +using LibHac.Fs; + +namespace LibHac.Common +{ + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal static class HResult + { + public const int ERROR_FILE_NOT_FOUND = unchecked((int)0x80070002); + public const int ERROR_PATH_NOT_FOUND = unchecked((int)0x80070003); + public const int ERROR_ACCESS_DENIED = unchecked((int)0x80070005); + public const int ERROR_SHARING_VIOLATION = unchecked((int)0x80070020); + public const int ERROR_HANDLE_EOF = unchecked((int)0x80070026); + public const int ERROR_HANDLE_DISK_FULL = unchecked((int)0x80070027); + public const int ERROR_FILE_EXISTS = unchecked((int)0x80070050); + public const int ERROR_DISK_FULL = unchecked((int)0x80070070); + public const int ERROR_INVALID_NAME = unchecked((int)0x8007007B); + public const int ERROR_DIR_NOT_EMPTY = unchecked((int)0x80070091); + public const int ERROR_ALREADY_EXISTS = unchecked((int)0x800700B7); + public const int ERROR_DIRECTORY = unchecked((int)0x8007010B); + public const int ERROR_SPACES_NOT_ENOUGH_DRIVES = unchecked((int)0x80E7000B); + + public static Result HResultToHorizonResult(int hResult) + { + return hResult switch + { + ERROR_FILE_NOT_FOUND => ResultFs.PathNotFound, + ERROR_PATH_NOT_FOUND => ResultFs.PathNotFound, + ERROR_ACCESS_DENIED => ResultFs.TargetLocked, + ERROR_SHARING_VIOLATION => ResultFs.TargetLocked, + ERROR_HANDLE_EOF => ResultFs.ValueOutOfRange, + ERROR_HANDLE_DISK_FULL => ResultFs.InsufficientFreeSpace, + ERROR_FILE_EXISTS => ResultFs.PathAlreadyExists, + ERROR_DISK_FULL => ResultFs.InsufficientFreeSpace, + ERROR_INVALID_NAME => ResultFs.PathNotFound, + ERROR_DIR_NOT_EMPTY => ResultFs.DirectoryNotEmpty, + ERROR_ALREADY_EXISTS => ResultFs.PathAlreadyExists, + ERROR_DIRECTORY => ResultFs.PathNotFound, + ERROR_SPACES_NOT_ENOUGH_DRIVES => ResultFs.InsufficientFreeSpace, + _ => ResultFs.UnknownHostFileSystemError + }; + } + } +} diff --git a/src/LibHac/Common/ITimeStampGenerator.cs b/src/LibHac/Common/ITimeStampGenerator.cs new file mode 100644 index 00000000..3d0c528b --- /dev/null +++ b/src/LibHac/Common/ITimeStampGenerator.cs @@ -0,0 +1,9 @@ +using System; + +namespace LibHac.Common +{ + public interface ITimeStampGenerator + { + DateTimeOffset Generate(); + } +} \ No newline at end of file diff --git a/src/LibHac/Common/Id128.cs b/src/LibHac/Common/Id128.cs new file mode 100644 index 00000000..2cb598e6 --- /dev/null +++ b/src/LibHac/Common/Id128.cs @@ -0,0 +1,87 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace LibHac.Common +{ + /// + /// A generic 128-bit ID value. + /// + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + public struct Id128 : IEquatable, IComparable, IComparable + { + public readonly ulong High; + public readonly ulong Low; + + public static Id128 Zero => default; + + public Id128(ulong high, ulong low) + { + High = high; + Low = low; + } + + public Id128(ReadOnlySpan uid) + { + ReadOnlySpan longs = MemoryMarshal.Cast(uid); + + High = longs[0]; + Low = longs[1]; + } + + public override string ToString() => AsBytes().ToHexString(); + + public bool Equals(Id128 other) + { + return High == other.High && Low == other.Low; + } + + public override bool Equals(object obj) + { + return obj is Id128 other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (High.GetHashCode() * 397) ^ Low.GetHashCode(); + } + } + + public int CompareTo(Id128 other) + { + int highComparison = High.CompareTo(other.High); + if (highComparison != 0) return highComparison; + return Low.CompareTo(other.Low); + } + + public int CompareTo(object obj) + { + if (obj is null) return 1; + return obj is Id128 other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(Id128)}"); + } + + public readonly void ToBytes(Span output) + { + Span longs = MemoryMarshal.Cast(output); + + longs[0] = High; + longs[1] = Low; + } + + public ReadOnlySpan AsBytes() + { + return SpanHelpers.AsByteSpan(ref this); + } + + public static bool operator ==(Id128 left, Id128 right) => left.Equals(right); + public static bool operator !=(Id128 left, Id128 right) => !left.Equals(right); + + public static bool operator <(Id128 left, Id128 right) => left.CompareTo(right) < 0; + public static bool operator >(Id128 left, Id128 right) => left.CompareTo(right) > 0; + public static bool operator <=(Id128 left, Id128 right) => left.CompareTo(right) <= 0; + public static bool operator >=(Id128 left, Id128 right) => left.CompareTo(right) >= 0; + } +} diff --git a/src/LibHac/Common/Key128.cs b/src/LibHac/Common/Key128.cs new file mode 100644 index 00000000..6febda1a --- /dev/null +++ b/src/LibHac/Common/Key128.cs @@ -0,0 +1,48 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace LibHac.Common +{ + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + public struct Key128 : IEquatable + { + private readonly ulong _dummy1; + private readonly ulong _dummy2; + + public Span Value => SpanHelpers.AsByteSpan(ref this); + + public Key128(ReadOnlySpan bytes) + { + ReadOnlySpan longs = MemoryMarshal.Cast(bytes); + + _dummy1 = longs[0]; + _dummy2 = longs[1]; + } + + public override string ToString() => Value.ToHexString(); + + public override bool Equals(object obj) + { + return obj is Key128 key && Equals(key); + } + + public bool Equals(Key128 other) + { + return _dummy1 == other._dummy1 && + _dummy2 == other._dummy2; + } + + public override int GetHashCode() + { + int hashCode = -1653217991; + hashCode = hashCode * -1521134295 + _dummy1.GetHashCode(); + hashCode = hashCode * -1521134295 + _dummy2.GetHashCode(); + return hashCode; + } + + public static bool operator ==(Key128 left, Key128 right) => left.Equals(right); + public static bool operator !=(Key128 left, Key128 right) => !(left == right); + } +} diff --git a/src/LibHac/Common/PaddingStructs.cs b/src/LibHac/Common/PaddingStructs.cs new file mode 100644 index 00000000..92ac14d7 --- /dev/null +++ b/src/LibHac/Common/PaddingStructs.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace LibHac.Common +{ + // In order for the Visual Studio debugger to accurately display a struct, every offset + // in the struct that is used for the debugger display must be part of a field. + // These padding structs make it easier to accomplish that. + [StructLayout(LayoutKind.Sequential, Size = 0x20)] + internal struct Padding20 + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly ulong Padding00; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly ulong Padding08; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly ulong Padding10; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly ulong Padding18; + } + + [StructLayout(LayoutKind.Sequential, Size = 0x40)] + internal struct Padding40 + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding20 Padding00; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding20 Padding20; + } + + [StructLayout(LayoutKind.Sequential, Size = 0x80)] + internal struct Padding80 + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding40 Padding00; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding40 Padding40; + } + + [StructLayout(LayoutKind.Sequential, Size = 0x100)] + internal struct Padding100 + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding80 Padding00; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding80 Padding80; + } +} diff --git a/src/LibHac/Common/SpanHelpers.cs b/src/LibHac/Common/SpanHelpers.cs new file mode 100644 index 00000000..ce8aabd4 --- /dev/null +++ b/src/LibHac/Common/SpanHelpers.cs @@ -0,0 +1,35 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace LibHac.Common +{ + public static class SpanHelpers + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NETCOREAPP + public static Span CreateSpan(ref T reference, int length) + { + return MemoryMarshal.CreateSpan(ref reference, length); + } +#else + public static unsafe Span CreateSpan(ref T reference, int length) + { + return new Span(Unsafe.AsPointer(ref reference), length); + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsSpan(ref T reference) where T : unmanaged + { + return CreateSpan(ref reference, 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span AsByteSpan(ref T reference) where T : unmanaged + { + Span span = AsSpan(ref reference); + return MemoryMarshal.Cast(span); + } + } +} diff --git a/src/LibHac/Common/StringUtils.cs b/src/LibHac/Common/StringUtils.cs new file mode 100644 index 00000000..df4443c9 --- /dev/null +++ b/src/LibHac/Common/StringUtils.cs @@ -0,0 +1,95 @@ +using System; +using System.Text; + +namespace LibHac.Common +{ + public static class StringUtils + { + public static int Copy(Span dest, ReadOnlySpan source) + { + int maxLen = Math.Min(dest.Length, source.Length); + + int i; + for (i = 0; i < maxLen && source[i] != 0; i++) + dest[i] = source[i]; + + if (i < dest.Length) + { + dest[i] = 0; + } + + return i; + } + + public static int GetLength(ReadOnlySpan s) + { + int i = 0; + + while (i < s.Length && s[i] != 0) + { + i++; + } + + return i; + } + + public static int GetLength(ReadOnlySpan s, int maxLen) + { + int i = 0; + + while (i < maxLen && i < s.Length && s[i] != 0) + { + i++; + } + + return i; + } + + /// + /// Concatenates 2 byte strings. + /// + /// + /// + /// The length of the resulting string. + /// This function appends the source string to the end of the null-terminated destination string. + /// If the destination buffer is not large enough to contain the resulting string, + /// bytes from the source string will be appended to the destination string util the buffer is full. + /// If the length of the final string is the same length of the destination buffer, + /// no null terminating byte will be written to the end of the string. + public static int Concat(Span dest, ReadOnlySpan source) + { + return Concat(dest, GetLength(dest), source); + } + + public static int Concat(Span dest, int destLength, ReadOnlySpan source) + { + int iDest = destLength; + + for (int i = 0; iDest < dest.Length && i < source.Length && source[i] != 0; i++, iDest++) + { + dest[iDest] = source[i]; + } + + if (iDest < dest.Length) + { + dest[iDest] = 0; + } + + return iDest; + } + + public static string Utf8ToString(ReadOnlySpan value) + { +#if STRING_SPAN + return Encoding.UTF8.GetString(value); +#else + return Encoding.UTF8.GetString(value.ToArray()); +#endif + } + + public static string Utf8ZToString(ReadOnlySpan value) + { + return Utf8ToString(value.Slice(0, GetLength(value))); + } + } +} diff --git a/src/LibHac/Common/U8Span.cs b/src/LibHac/Common/U8Span.cs new file mode 100644 index 00000000..c58aa3e7 --- /dev/null +++ b/src/LibHac/Common/U8Span.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using System.Text; + +namespace LibHac.Common +{ + [DebuggerDisplay("{ToString()}")] + public ref struct U8Span + { + private readonly ReadOnlySpan _buffer; + + public ReadOnlySpan Value => _buffer; + public int Length => _buffer.Length; + + public byte this[int i] + { + get => _buffer[i]; + } + + public U8Span(ReadOnlySpan value) + { + _buffer = value; + } + + public U8Span(string value) + { + _buffer = Encoding.UTF8.GetBytes(value); + } + + public static implicit operator ReadOnlySpan(U8Span value) => value.Value; + + public static explicit operator string(U8Span value) => value.ToString(); + public static explicit operator U8Span(string value) => new U8Span(value); + + public override string ToString() + { + return StringUtils.Utf8ToString(_buffer); + } + + public bool IsNull() => _buffer == default; + } +} diff --git a/src/LibHac/Common/U8SpanMutable.cs b/src/LibHac/Common/U8SpanMutable.cs new file mode 100644 index 00000000..d0edcfa1 --- /dev/null +++ b/src/LibHac/Common/U8SpanMutable.cs @@ -0,0 +1,46 @@ +using System; +using System.Diagnostics; +using System.Text; + +namespace LibHac.Common +{ + [DebuggerDisplay("{ToString()}")] + public ref struct U8SpanMutable + { + private readonly Span _buffer; + + public Span Value => _buffer; + public int Length => _buffer.Length; + + public byte this[int i] + { + get => _buffer[i]; + set => _buffer[i] = value; + } + + public U8SpanMutable(Span value) + { + _buffer = value; + } + + public U8SpanMutable(string value) + { + _buffer = Encoding.UTF8.GetBytes(value); + } + + public static implicit operator U8Span(U8SpanMutable value) => new U8Span(value._buffer); + + public static implicit operator ReadOnlySpan(U8SpanMutable value) => value.Value; + public static implicit operator Span(U8SpanMutable value) => value.Value; + + public static explicit operator string(U8SpanMutable value) => value.ToString(); + public static explicit operator U8SpanMutable(string value) => new U8SpanMutable(value); + + public override string ToString() + { + return StringUtils.Utf8ZToString(_buffer); + } + + public bool IsNull() => _buffer == default; + } +} diff --git a/src/LibHac/Common/U8String.cs b/src/LibHac/Common/U8String.cs new file mode 100644 index 00000000..df952cf5 --- /dev/null +++ b/src/LibHac/Common/U8String.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics; +using System.Text; + +namespace LibHac.Common +{ + [DebuggerDisplay("{ToString()}")] + public struct U8String + { + private readonly byte[] _buffer; + + public ReadOnlySpan Value => _buffer; + public int Length => _buffer.Length; + + public byte this[int i] => _buffer[i]; + + public U8String(byte[] value) + { + _buffer = value; + } + + public U8String(string value) + { + _buffer = Encoding.UTF8.GetBytes(value); + } + + public static implicit operator U8Span(U8String value) => new U8Span(value._buffer); + + public static implicit operator ReadOnlySpan(U8String value) => value.Value; + + public static explicit operator string(U8String value) => value.ToString(); + public static explicit operator U8String(string value) => new U8String(value); + + public override string ToString() + { + return StringUtils.Utf8ToString(_buffer); + } + + public bool IsNull() => _buffer == null; + } +} diff --git a/src/LibHac/Common/U8StringBuilder.cs b/src/LibHac/Common/U8StringBuilder.cs new file mode 100644 index 00000000..cbaed2f1 --- /dev/null +++ b/src/LibHac/Common/U8StringBuilder.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics; + +namespace LibHac.Common +{ + [DebuggerDisplay("{ToString()}")] + public ref struct U8StringBuilder + { + private const int NullTerminatorLength = 1; + + private readonly Span _buffer; + private int _length; + + public bool Overflowed { get; private set; } + public int Capacity => _buffer.Length - NullTerminatorLength; + + public U8StringBuilder(Span buffer) + { + _buffer = buffer; + _length = 0; + Overflowed = false; + + ThrowIfBufferLengthIsZero(); + + AddNullTerminator(); + } + + public U8StringBuilder Append(ReadOnlySpan value) + { + if (Overflowed) return this; + + int valueLength = StringUtils.GetLength(value); + + if (!HasAdditionalCapacity(valueLength)) + { + Overflowed = true; + return this; + } + + value.Slice(0, valueLength).CopyTo(_buffer.Slice(_length)); + _length += valueLength; + AddNullTerminator(); + + return this; + } + + public U8StringBuilder Append(byte value) + { + if (Overflowed) return this; + + if (!HasAdditionalCapacity(1)) + { + Overflowed = true; + return this; + } + + _buffer[_length] = value; + _length++; + AddNullTerminator(); + + return this; + } + + private bool HasCapacity(int requiredCapacity) + { + return requiredCapacity <= Capacity; + } + + private bool HasAdditionalCapacity(int requiredAdditionalCapacity) + { + return HasCapacity(_length + requiredAdditionalCapacity); + } + + private void AddNullTerminator() + { + _buffer[_length] = 0; + } + + private void ThrowIfBufferLengthIsZero() + { + if (_buffer.Length == 0) throw new ArgumentException("Buffer length must be greater than 0."); + } + + public override string ToString() => StringUtils.Utf8ZToString(_buffer); + } +} diff --git a/src/LibHac/Common/U8StringHelpers.cs b/src/LibHac/Common/U8StringHelpers.cs new file mode 100644 index 00000000..5dcae1ce --- /dev/null +++ b/src/LibHac/Common/U8StringHelpers.cs @@ -0,0 +1,15 @@ +namespace LibHac.Common +{ + public static class U8StringHelpers + { + public static U8String ToU8String(this string value) + { + return new U8String(value); + } + + public static U8Span ToU8Span(this string value) + { + return new U8Span(value); + } + } +} diff --git a/src/LibHac/Common/U8StringMutable.cs b/src/LibHac/Common/U8StringMutable.cs new file mode 100644 index 00000000..2bb7c75b --- /dev/null +++ b/src/LibHac/Common/U8StringMutable.cs @@ -0,0 +1,48 @@ +using System; +using System.Diagnostics; +using System.Text; + +namespace LibHac.Common +{ + [DebuggerDisplay("{ToString()}")] + public struct U8StringMutable + { + private readonly byte[] _buffer; + + public Span Value => _buffer; + public int Length => _buffer.Length; + + public byte this[int i] + { + get => _buffer[i]; + set => _buffer[i] = value; + } + + public U8StringMutable(byte[] value) + { + _buffer = value; + } + + public U8StringMutable(string value) + { + _buffer = Encoding.UTF8.GetBytes(value); + } + + public static implicit operator U8String(U8StringMutable value) => new U8String(value._buffer); + public static implicit operator U8SpanMutable(U8StringMutable value) => new U8SpanMutable(value._buffer); + public static implicit operator U8Span(U8StringMutable value) => new U8Span(value._buffer); + + public static implicit operator ReadOnlySpan(U8StringMutable value) => value.Value; + public static implicit operator Span(U8StringMutable value) => value.Value; + + public static explicit operator string(U8StringMutable value) => value.ToString(); + public static explicit operator U8StringMutable(string value) => new U8StringMutable(value); + + public override string ToString() + { + return StringUtils.Utf8ZToString(_buffer); + } + + public bool IsNull() => _buffer == null; + } +} diff --git a/src/LibHac/Crypto.cs b/src/LibHac/Crypto.cs index 3ae39d49..62c859ba 100644 --- a/src/LibHac/Crypto.cs +++ b/src/LibHac/Crypto.cs @@ -2,7 +2,7 @@ using System.IO; using System.Numerics; using System.Security.Cryptography; -using LibHac.Fs; +using LibHac.FsSystem; namespace LibHac { diff --git a/src/LibHac/Fs/AccessLogHelpers.cs b/src/LibHac/Fs/AccessLogHelpers.cs new file mode 100644 index 00000000..7fd00695 --- /dev/null +++ b/src/LibHac/Fs/AccessLogHelpers.cs @@ -0,0 +1,14 @@ +using System; + +namespace LibHac.Fs +{ + public static class AccessLogHelpers + { + public static string BuildDefaultLogLine(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, + string message, string caller) + { + return + $"FS_ACCESS: {{ start: {(long) startTime.TotalMilliseconds,9}, end: {(long) endTime.TotalMilliseconds,9}, result: 0x{result.Value:x8}, handle: 0x{handleId:x8}, function: \"{caller}\"{message} }}"; + } + } +} diff --git a/src/LibHac/Fs/Accessors/DirectoryAccessor.cs b/src/LibHac/Fs/Accessors/DirectoryAccessor.cs index e3b3141e..75cf7b25 100644 --- a/src/LibHac/Fs/Accessors/DirectoryAccessor.cs +++ b/src/LibHac/Fs/Accessors/DirectoryAccessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using LibHac.FsSystem; namespace LibHac.Fs.Accessors { @@ -9,24 +10,34 @@ namespace LibHac.Fs.Accessors public FileSystemAccessor Parent { get; } - public DirectoryAccessor(IDirectory baseDirectory, FileSystemAccessor parent) + private IFileSystem ParentFs { get; } + private string Path { get; } + + public DirectoryAccessor(IDirectory baseDirectory, FileSystemAccessor parent, IFileSystem parentFs, string path) { Directory = baseDirectory; Parent = parent; + ParentFs = parentFs; + Path = path; } - public IEnumerable Read() + public IEnumerable Read() { CheckIfDisposed(); - return Directory.Read(); + return ParentFs.EnumerateEntries(Path, "*", SearchOptions.Default); } - public int GetEntryCount() + public Result Read(out long entriesRead, Span entryBuffer) + { + return Directory.Read(out entriesRead, entryBuffer); + } + + public Result GetEntryCount(out long entryCount) { CheckIfDisposed(); - return Directory.GetEntryCount(); + return Directory.GetEntryCount(out entryCount); } public void Dispose() diff --git a/src/LibHac/Fs/Accessors/FileAccessor.cs b/src/LibHac/Fs/Accessors/FileAccessor.cs index a269f639..030a0e8b 100644 --- a/src/LibHac/Fs/Accessors/FileAccessor.cs +++ b/src/LibHac/Fs/Accessors/FileAccessor.cs @@ -2,7 +2,7 @@ namespace LibHac.Fs.Accessors { - public class FileAccessor : IFile + public class FileAccessor : FileBase { private IFile File { get; set; } @@ -10,9 +10,6 @@ namespace LibHac.Fs.Accessors public WriteState WriteState { get; private set; } public OpenMode OpenMode { get; } - // Todo: Consider removing Mode from interface because OpenMode is in FileAccessor - OpenMode IFile.Mode => OpenMode; - public FileAccessor(IFile baseFile, FileSystemAccessor parent, OpenMode mode) { File = baseFile; @@ -20,14 +17,14 @@ namespace LibHac.Fs.Accessors OpenMode = mode; } - public int Read(Span destination, long offset, ReadOption options) + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) { CheckIfDisposed(); - return File.Read(destination, offset, options); + return File.Read(out bytesRead, offset, destination, options); } - public void Write(ReadOnlySpan source, long offset, WriteOption options) + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) { CheckIfDisposed(); @@ -35,38 +32,48 @@ namespace LibHac.Fs.Accessors { WriteState = (WriteState)(~options & WriteOption.Flush); - return; + return Result.Success; } - File.Write(source, offset, options); + Result rc = File.Write(offset, source, options); - WriteState = (WriteState)(~options & WriteOption.Flush); + if (rc.IsSuccess()) + { + WriteState = (WriteState)(~options & WriteOption.Flush); + } + + return rc; } - public void Flush() + protected override Result FlushImpl() { CheckIfDisposed(); - File.Flush(); + Result rc = File.Flush(); - WriteState = WriteState.None; + if (rc.IsSuccess()) + { + WriteState = WriteState.None; + } + + return rc; } - public long GetSize() + protected override Result GetSizeImpl(out long size) { CheckIfDisposed(); - return File.GetSize(); + return File.GetSize(out size); } - public void SetSize(long size) + protected override Result SetSizeImpl(long size) { CheckIfDisposed(); - File.SetSize(size); + return File.SetSize(size); } - public void Dispose() + protected override void Dispose(bool disposing) { if (File == null) return; @@ -74,7 +81,7 @@ namespace LibHac.Fs.Accessors { // Original FS code would return an error: // ThrowHelper.ThrowResult(ResultsFs.ResultFsWriteStateUnflushed); - + Flush(); } diff --git a/src/LibHac/Fs/Accessors/FileSystemAccessor.cs b/src/LibHac/Fs/Accessors/FileSystemAccessor.cs index 3e8293fd..a51f1e6e 100644 --- a/src/LibHac/Fs/Accessors/FileSystemAccessor.cs +++ b/src/LibHac/Fs/Accessors/FileSystemAccessor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using LibHac.FsSystem; namespace LibHac.Fs.Accessors { @@ -9,7 +10,8 @@ namespace LibHac.Fs.Accessors public string Name { get; } private IFileSystem FileSystem { get; } - internal FileSystemManager FsManager { get; } + internal FileSystemClient FsClient { get; } + private ICommonMountNameGenerator MountNameGenerator { get; } private HashSet OpenFiles { get; } = new HashSet(); private HashSet OpenDirectories { get; } = new HashSet(); @@ -18,124 +20,140 @@ namespace LibHac.Fs.Accessors internal bool IsAccessLogEnabled { get; set; } - public FileSystemAccessor(string name, IFileSystem baseFileSystem, FileSystemManager fsManager) + public FileSystemAccessor(string name, IFileSystem baseFileSystem, FileSystemClient fsClient, ICommonMountNameGenerator nameGenerator) { Name = name; FileSystem = baseFileSystem; - FsManager = fsManager; + FsClient = fsClient; + MountNameGenerator = nameGenerator; } - public void CreateDirectory(string path) + public Result CreateDirectory(string path) { - FileSystem.CreateDirectory(path); + return FileSystem.CreateDirectory(path); } - public void CreateFile(string path, long size, CreateFileOptions options) + public Result CreateFile(string path, long size, CreateFileOptions options) { - FileSystem.CreateFile(path, size, options); + return FileSystem.CreateFile(path, size, options); } - public void DeleteDirectory(string path) + public Result DeleteDirectory(string path) { - FileSystem.DeleteDirectory(path); + return FileSystem.DeleteDirectory(path); } - public void DeleteDirectoryRecursively(string path) + public Result DeleteDirectoryRecursively(string path) { - FileSystem.DeleteDirectoryRecursively(path); + return FileSystem.DeleteDirectoryRecursively(path); } - public void CleanDirectoryRecursively(string path) + public Result CleanDirectoryRecursively(string path) { - FileSystem.CleanDirectoryRecursively(path); + return FileSystem.CleanDirectoryRecursively(path); } - public void DeleteFile(string path) + public Result DeleteFile(string path) { - FileSystem.DeleteFile(path); + return FileSystem.DeleteFile(path); } - public DirectoryAccessor OpenDirectory(string path, OpenDirectoryMode mode) + public Result OpenDirectory(out DirectoryAccessor directory, string path, OpenDirectoryMode mode) { - IDirectory dir = FileSystem.OpenDirectory(path, mode); + directory = default; - var accessor = new DirectoryAccessor(dir, this); + Result rc = FileSystem.OpenDirectory(out IDirectory rawDirectory, path, mode); + if (rc.IsFailure()) return rc; + + var accessor = new DirectoryAccessor(rawDirectory, this, FileSystem, path); lock (_locker) { OpenDirectories.Add(accessor); } - return accessor; + directory = accessor; + return Result.Success; } - public FileAccessor OpenFile(string path, OpenMode mode) + public Result OpenFile(out FileAccessor file, string path, OpenMode mode) { - IFile file = FileSystem.OpenFile(path, mode); + file = default; - var accessor = new FileAccessor(file, this, mode); + Result rc = FileSystem.OpenFile(out IFile rawFile, path, mode); + if (rc.IsFailure()) return rc; + + var accessor = new FileAccessor(rawFile, this, mode); lock (_locker) { OpenFiles.Add(accessor); } - return accessor; + file = accessor; + return Result.Success; } - public void RenameDirectory(string srcPath, string dstPath) + public Result RenameDirectory(string oldPath, string newPath) { - FileSystem.RenameDirectory(srcPath, dstPath); + return FileSystem.RenameDirectory(oldPath, newPath); } - public void RenameFile(string srcPath, string dstPath) + public Result RenameFile(string oldPath, string newPath) { - FileSystem.RenameFile(srcPath, dstPath); + return FileSystem.RenameFile(oldPath, newPath); } - public void DirectoryExists(string path) + public bool DirectoryExists(string path) { - FileSystem.DirectoryExists(path); + return FileSystem.DirectoryExists(path); } - public void FileExists(string path) + public bool FileExists(string path) { - FileSystem.FileExists(path); + return FileSystem.FileExists(path); } - public DirectoryEntryType GetEntryType(string path) + public Result GetEntryType(out DirectoryEntryType type, string path) { - return FileSystem.GetEntryType(path); + return FileSystem.GetEntryType(out type, path); } - public long GetFreeSpaceSize(string path) + public Result GetFreeSpaceSize(out long freeSpace, string path) { - return FileSystem.GetFreeSpaceSize(path); + return FileSystem.GetFreeSpaceSize(out freeSpace, path); } - public long GetTotalSpaceSize(string path) + public Result GetTotalSpaceSize(out long totalSpace, string path) { - return FileSystem.GetTotalSpaceSize(path); + return FileSystem.GetTotalSpaceSize(out totalSpace, path); } - public FileTimeStampRaw GetFileTimeStampRaw(string path) + public Result GetFileTimeStampRaw(out FileTimeStampRaw timeStamp, string path) { - return FileSystem.GetFileTimeStampRaw(path); + return FileSystem.GetFileTimeStampRaw(out timeStamp, path); } - public void Commit() + public Result Commit() { if (OpenFiles.Any(x => (x.OpenMode & OpenMode.Write) != 0)) { - ThrowHelper.ThrowResult(ResultFs.WritableFileOpen); + return ResultFs.WritableFileOpen.Log(); } - FileSystem.Commit(); + return FileSystem.Commit(); } - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) + public Result QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) { - FileSystem.QueryEntry(outBuffer, inBuffer, path, queryId); + return FileSystem.QueryEntry(outBuffer, inBuffer, queryId, path); + } + + public Result GetCommonMountName(Span nameBuffer) + { + if (MountNameGenerator == null) return ResultFs.PreconditionViolation; + + return MountNameGenerator.Generate(nameBuffer); } internal void NotifyCloseFile(FileAccessor file) diff --git a/src/LibHac/Fs/Accessors/IAccessLog.cs b/src/LibHac/Fs/Accessors/IAccessLog.cs deleted file mode 100644 index 3ff9ed53..00000000 --- a/src/LibHac/Fs/Accessors/IAccessLog.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace LibHac.Fs.Accessors -{ - public interface IAccessLog - { - void Log(TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = ""); - } -} \ No newline at end of file diff --git a/src/LibHac/Fs/AesXtsDirectory.cs b/src/LibHac/Fs/AesXtsDirectory.cs deleted file mode 100644 index 99caa17a..00000000 --- a/src/LibHac/Fs/AesXtsDirectory.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace LibHac.Fs -{ - public class AesXtsDirectory : IDirectory - { - IFileSystem IDirectory.ParentFileSystem => ParentFileSystem; - public AesXtsFileSystem ParentFileSystem { get; } - - public string FullPath { get; } - public OpenDirectoryMode Mode { get; } - - private IFileSystem BaseFileSystem { get; } - private IDirectory BaseDirectory { get; } - - public AesXtsDirectory(AesXtsFileSystem parentFs, IDirectory baseDir, OpenDirectoryMode mode) - { - ParentFileSystem = parentFs; - BaseDirectory = baseDir; - Mode = mode; - BaseFileSystem = BaseDirectory.ParentFileSystem; - FullPath = BaseDirectory.FullPath; - } - - public IEnumerable Read() - { - foreach (DirectoryEntry entry in BaseDirectory.Read()) - { - if (entry.Type == DirectoryEntryType.Directory) - { - yield return entry; - } - else - { - long size = GetAesXtsFileSize(entry.FullPath); - if (size == -1) continue; - - yield return new DirectoryEntry(entry.Name, entry.FullPath, entry.Type, size); - } - } - } - - public int GetEntryCount() - { - return BaseDirectory.GetEntryCount(); - } - - /// - /// Reads the size of a NAX0 file from its header. Returns -1 on error. - /// - /// - /// - private long GetAesXtsFileSize(string path) - { - try - { - using (IFile file = BaseFileSystem.OpenFile(path, OpenMode.Read)) - { - if (file.GetSize() < 0x50) - { - return -1; - } - - var buffer = new byte[8]; - - file.Read(buffer, 0x20); - if (BitConverter.ToUInt32(buffer, 0) != 0x3058414E) return 0; - - file.Read(buffer, 0x48); - return BitConverter.ToInt64(buffer, 0); - } - } - catch (ArgumentOutOfRangeException) - { - return -1; - } - } - } -} diff --git a/src/LibHac/Fs/AesXtsFileSystem.cs b/src/LibHac/Fs/AesXtsFileSystem.cs deleted file mode 100644 index facfbe5e..00000000 --- a/src/LibHac/Fs/AesXtsFileSystem.cs +++ /dev/null @@ -1,242 +0,0 @@ -using System; -using System.Diagnostics; - -namespace LibHac.Fs -{ - public class AesXtsFileSystem : IFileSystem - { - public int BlockSize { get; } - - private IFileSystem BaseFileSystem { get; } - private byte[] KekSource { get; } - private byte[] ValidationKey { get; } - - public AesXtsFileSystem(IFileSystem fs, byte[] kekSource, byte[] validationKey, int blockSize) - { - BaseFileSystem = fs; - KekSource = kekSource; - ValidationKey = validationKey; - BlockSize = blockSize; - } - - public AesXtsFileSystem(IFileSystem fs, byte[] keys, int blockSize) - { - BaseFileSystem = fs; - KekSource = keys.AsSpan(0, 0x10).ToArray(); - ValidationKey = keys.AsSpan(0x10, 0x10).ToArray(); - BlockSize = blockSize; - } - - public void CreateDirectory(string path) - { - BaseFileSystem.CreateDirectory(path); - } - - public void CreateFile(string path, long size, CreateFileOptions options) - { - CreateFile(path, size, options, new byte[0x20]); - } - - /// - /// Creates a new using the provided key. - /// - /// The full path of the file to create. - /// The initial size of the created file. - /// Flags to control how the file is created. - /// Should usually be - /// The 256-bit key containing a 128-bit data key followed by a 128-bit tweak key. - public void CreateFile(string path, long size, CreateFileOptions options, byte[] key) - { - long containerSize = AesXtsFile.HeaderLength + Util.AlignUp(size, 0x10); - BaseFileSystem.CreateFile(path, containerSize, options); - - var header = new AesXtsFileHeader(key, size, path, KekSource, ValidationKey); - - using (IFile baseFile = BaseFileSystem.OpenFile(path, OpenMode.Write)) - { - baseFile.Write(header.ToBytes(false), 0); - } - } - - public void DeleteDirectory(string path) - { - BaseFileSystem.DeleteDirectory(path); - } - - public void DeleteDirectoryRecursively(string path) - { - BaseFileSystem.DeleteDirectoryRecursively(path); - } - - public void CleanDirectoryRecursively(string path) - { - BaseFileSystem.CleanDirectoryRecursively(path); - } - - public void DeleteFile(string path) - { - BaseFileSystem.DeleteFile(path); - } - - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) - { - path = PathTools.Normalize(path); - - IDirectory baseDir = BaseFileSystem.OpenDirectory(path, mode); - - var dir = new AesXtsDirectory(this, baseDir, mode); - return dir; - } - - public IFile OpenFile(string path, OpenMode mode) - { - path = PathTools.Normalize(path); - - IFile baseFile = BaseFileSystem.OpenFile(path, mode | OpenMode.Read); - var file = new AesXtsFile(mode, baseFile, path, KekSource, ValidationKey, BlockSize); - - file.ToDispose.Add(baseFile); - return file; - } - - public void RenameDirectory(string srcPath, string dstPath) - { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); - - BaseFileSystem.RenameDirectory(srcPath, dstPath); - - try - { - RenameDirectoryImpl(srcPath, dstPath, false); - } - catch (Exception) - { - RenameDirectoryImpl(srcPath, dstPath, true); - BaseFileSystem.RenameDirectory(dstPath, srcPath); - - throw; - } - } - - private void RenameDirectoryImpl(string srcDir, string dstDir, bool doRollback) - { - IDirectory dir = OpenDirectory(dstDir, OpenDirectoryMode.All); - - foreach (DirectoryEntry entry in dir.Read()) - { - string subSrcPath = $"{srcDir}/{entry.Name}"; - string subDstPath = $"{dstDir}/{entry.Name}"; - - if (entry.Type == DirectoryEntryType.Directory) - { - RenameDirectoryImpl(subSrcPath, subDstPath, doRollback); - } - - if (entry.Type == DirectoryEntryType.File) - { - if (doRollback) - { - if (TryReadXtsHeader(subDstPath, subDstPath, out AesXtsFileHeader header)) - { - WriteXtsHeader(header, subDstPath, subSrcPath); - } - } - else - { - AesXtsFileHeader header = ReadXtsHeader(subDstPath, subSrcPath); - WriteXtsHeader(header, subDstPath, subDstPath); - } - } - } - } - - public void RenameFile(string srcPath, string dstPath) - { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); - - AesXtsFileHeader header = ReadXtsHeader(srcPath, srcPath); - - BaseFileSystem.RenameFile(srcPath, dstPath); - - try - { - WriteXtsHeader(header, dstPath, dstPath); - } - catch (Exception) - { - BaseFileSystem.RenameFile(dstPath, srcPath); - WriteXtsHeader(header, srcPath, srcPath); - - throw; - } - } - - public DirectoryEntryType GetEntryType(string path) - { - return BaseFileSystem.GetEntryType(path); - } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - return BaseFileSystem.GetFileTimeStampRaw(path); - } - - public long GetFreeSpaceSize(string path) - { - return BaseFileSystem.GetFreeSpaceSize(path); - } - - public long GetTotalSpaceSize(string path) - { - return BaseFileSystem.GetTotalSpaceSize(path); - } - - public void Commit() - { - BaseFileSystem.Commit(); - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) - { - BaseFileSystem.QueryEntry(outBuffer, inBuffer, path, queryId); - } - - private AesXtsFileHeader ReadXtsHeader(string filePath, string keyPath) - { - if (!TryReadXtsHeader(filePath, keyPath, out AesXtsFileHeader header)) - { - ThrowHelper.ThrowResult(ResultFs.AesXtsFileHeaderInvalidKeysInRenameFile, "Could not decrypt AES-XTS keys"); - } - - return header; - } - - private bool TryReadXtsHeader(string filePath, string keyPath, out AesXtsFileHeader header) - { - Debug.Assert(PathTools.IsNormalized(filePath.AsSpan())); - Debug.Assert(PathTools.IsNormalized(keyPath.AsSpan())); - - using (IFile file = BaseFileSystem.OpenFile(filePath, OpenMode.Read)) - { - header = new AesXtsFileHeader(file); - - return header.TryDecryptHeader(keyPath, KekSource, ValidationKey); - } - } - - private void WriteXtsHeader(AesXtsFileHeader header, string filePath, string keyPath) - { - Debug.Assert(PathTools.IsNormalized(filePath.AsSpan())); - Debug.Assert(PathTools.IsNormalized(keyPath.AsSpan())); - - header.EncryptHeader(keyPath, KekSource, ValidationKey); - - using (IFile file = BaseFileSystem.OpenFile(filePath, OpenMode.ReadWrite)) - { - file.Write(header.ToBytes(false), 0, WriteOption.Flush); - } - } - } -} diff --git a/src/LibHac/Fs/ConcatenationDirectory.cs b/src/LibHac/Fs/ConcatenationDirectory.cs deleted file mode 100644 index 770563f1..00000000 --- a/src/LibHac/Fs/ConcatenationDirectory.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; - -#if CROSS_PLATFORM -using System.Runtime.InteropServices; -#endif - -namespace LibHac.Fs -{ - public class ConcatenationDirectory : IDirectory - { - IFileSystem IDirectory.ParentFileSystem => ParentFileSystem; - public string FullPath { get; } - public OpenDirectoryMode Mode { get; } - - private ConcatenationFileSystem ParentFileSystem { get; } - private IDirectory ParentDirectory { get; } - - public ConcatenationDirectory(ConcatenationFileSystem fs, IDirectory parentDirectory, OpenDirectoryMode mode) - { - ParentFileSystem = fs; - ParentDirectory = parentDirectory; - Mode = mode; - FullPath = parentDirectory.FullPath; - } - - public IEnumerable Read() - { - foreach (DirectoryEntry entry in ParentDirectory.Read()) - { - bool isSplit = IsConcatenationFile(entry); - - if (!CanReturnEntry(entry, isSplit)) continue; - - if (isSplit) - { - entry.Type = DirectoryEntryType.File; - entry.Size = ParentFileSystem.GetConcatenationFileSize(entry.FullPath); - entry.Attributes = NxFileAttributes.None; - } - - yield return entry; - } - } - - public int GetEntryCount() - { - int count = 0; - - foreach (DirectoryEntry entry in ParentDirectory.Read()) - { - bool isSplit = IsConcatenationFile(entry); - - if (CanReturnEntry(entry, isSplit)) count++; - } - - return count; - } - - private bool CanReturnEntry(DirectoryEntry entry, bool isSplit) - { - return Mode.HasFlag(OpenDirectoryMode.Files) && (entry.Type == DirectoryEntryType.File || isSplit) || - Mode.HasFlag(OpenDirectoryMode.Directories) && entry.Type == DirectoryEntryType.Directory && !isSplit; - } - - private bool IsConcatenationFile(DirectoryEntry entry) - { -#if CROSS_PLATFORM - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return ConcatenationFileSystem.HasConcatenationFileAttribute(entry.Attributes); - } - else - { - return ParentFileSystem.IsConcatenationFile(entry.FullPath); - } -#else - return ConcatenationFileSystem.HasConcatenationFileAttribute(entry.Attributes); -#endif - } - } -} diff --git a/src/LibHac/Fs/ConcatenationFileSystem.cs b/src/LibHac/Fs/ConcatenationFileSystem.cs deleted file mode 100644 index fb242849..00000000 --- a/src/LibHac/Fs/ConcatenationFileSystem.cs +++ /dev/null @@ -1,325 +0,0 @@ -using System; -using System.Collections.Generic; - -#if CROSS_PLATFORM -using System.Runtime.InteropServices; -#endif - -namespace LibHac.Fs -{ - /// - /// An that stores large files as smaller, separate sub-files. - /// - /// - /// This filesystem is mainly used to allow storing large files on filesystems that have low - /// limits on file size such as FAT filesystems. The underlying base filesystem must have - /// support for the "Archive" file attribute found in FAT or NTFS filesystems. - /// - /// A may contain both standard files or Concatenation files. - /// If a directory has the archive attribute set, its contents will be concatenated and treated - /// as a single file. These sub-files must follow the naming scheme "00", "01", "02", ... - /// Each sub-file except the final one must have the size that was specified - /// at the creation of the . - /// - public class ConcatenationFileSystem : IFileSystem - { - private const long DefaultSubFileSize = 0xFFFF0000; // Hard-coded value used by FS - private IAttributeFileSystem BaseFileSystem { get; } - private long SubFileSize { get; } - - /// - /// Initializes a new . - /// - /// The base for the - /// new . - public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem) : this(baseFileSystem, DefaultSubFileSize) { } - - /// - /// Initializes a new . - /// - /// The base for the - /// new . - /// The size of each sub-file. Once a file exceeds this size, a new sub-file will be created - public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem, long subFileSize) - { - BaseFileSystem = baseFileSystem; - SubFileSize = subFileSize; - } - - // .NET Core on platforms other than Windows doesn't support getting the - // archive flag in FAT file systems. Try to work around that for now for reading, - // but writing still won't work properly on those platforms - internal bool IsConcatenationFile(string path) - { -#if CROSS_PLATFORM - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return HasConcatenationFileAttribute(BaseFileSystem.GetFileAttributes(path)); - } - else - { - return IsConcatenationFileHeuristic(path); - } -#else - return HasConcatenationFileAttribute(BaseFileSystem.GetFileAttributes(path)); -#endif - } - -#if CROSS_PLATFORM - private bool IsConcatenationFileHeuristic(string path) - { - if (BaseFileSystem.GetEntryType(path) != DirectoryEntryType.Directory) return false; - - if (BaseFileSystem.GetEntryType(PathTools.Combine(path, "00")) != DirectoryEntryType.File) return false; - - if (BaseFileSystem.OpenDirectory(path, OpenDirectoryMode.Directories).GetEntryCount() > 0) return false; - - // Should be enough checks to avoid most false positives. Maybe - return true; - } -#endif - - internal static bool HasConcatenationFileAttribute(NxFileAttributes attributes) - { - return (attributes & NxFileAttributes.Directory) != 0 && (attributes & NxFileAttributes.Archive) != 0; - } - - private void SetConcatenationFileAttribute(string path) - { - NxFileAttributes attributes = BaseFileSystem.GetFileAttributes(path); - attributes |= NxFileAttributes.Archive; - BaseFileSystem.SetFileAttributes(path, attributes); - } - - public void CreateDirectory(string path) - { - path = PathTools.Normalize(path); - string parent = PathTools.GetParentDirectory(path); - - if (IsConcatenationFile(parent)) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, - "Cannot create a directory inside of a concatenation file"); - } - - BaseFileSystem.CreateDirectory(path); - } - - public void CreateFile(string path, long size, CreateFileOptions options) - { - path = PathTools.Normalize(path); - - CreateFileOptions newOptions = options & ~CreateFileOptions.CreateConcatenationFile; - - if (!options.HasFlag(CreateFileOptions.CreateConcatenationFile)) - { - BaseFileSystem.CreateFile(path, size, newOptions); - return; - } - - // A concatenation file directory can't contain normal files - string parentDir = PathTools.GetParentDirectory(path); - - if (IsConcatenationFile(parentDir)) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, - "Cannot create a concatenation file inside of a concatenation file"); - } - - BaseFileSystem.CreateDirectory(path); - SetConcatenationFileAttribute(path); - - long remaining = size; - - for (int i = 0; remaining > 0; i++) - { - long fileSize = Math.Min(SubFileSize, remaining); - string fileName = GetSubFilePath(path, i); - - BaseFileSystem.CreateFile(fileName, fileSize, CreateFileOptions.None); - - remaining -= fileSize; - } - } - - public void DeleteDirectory(string path) - { - path = PathTools.Normalize(path); - - if (IsConcatenationFile(path)) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - BaseFileSystem.DeleteDirectory(path); - } - - public void DeleteDirectoryRecursively(string path) - { - path = PathTools.Normalize(path); - - if (IsConcatenationFile(path)) ThrowHelper.ThrowResult(ResultFs.PathNotFound); - - BaseFileSystem.DeleteDirectoryRecursively(path); - } - - public void CleanDirectoryRecursively(string path) - { - path = PathTools.Normalize(path); - - if (IsConcatenationFile(path)) ThrowHelper.ThrowResult(ResultFs.PathNotFound); - - BaseFileSystem.CleanDirectoryRecursively(path); - } - - public void DeleteFile(string path) - { - path = PathTools.Normalize(path); - - if (!IsConcatenationFile(path)) - { - BaseFileSystem.DeleteFile(path); - } - - int count = GetSubFileCount(path); - - for (int i = 0; i < count; i++) - { - BaseFileSystem.DeleteFile(GetSubFilePath(path, i)); - } - - BaseFileSystem.DeleteDirectory(path); - } - - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) - { - path = PathTools.Normalize(path); - - if (IsConcatenationFile(path)) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - IDirectory parentDir = BaseFileSystem.OpenDirectory(path, OpenDirectoryMode.All); - var dir = new ConcatenationDirectory(this, parentDir, mode); - return dir; - } - - public IFile OpenFile(string path, OpenMode mode) - { - path = PathTools.Normalize(path); - - if (!IsConcatenationFile(path)) - { - return BaseFileSystem.OpenFile(path, mode); - } - - int fileCount = GetSubFileCount(path); - - var files = new List(); - - for (int i = 0; i < fileCount; i++) - { - string filePath = GetSubFilePath(path, i); - IFile file = BaseFileSystem.OpenFile(filePath, mode); - files.Add(file); - } - - return new ConcatenationFile(BaseFileSystem, path, files, SubFileSize, mode); - } - - public void RenameDirectory(string srcPath, string dstPath) - { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); - - if (IsConcatenationFile(srcPath)) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - BaseFileSystem.RenameDirectory(srcPath, dstPath); - } - - public void RenameFile(string srcPath, string dstPath) - { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); - - if (IsConcatenationFile(srcPath)) - { - BaseFileSystem.RenameDirectory(srcPath, dstPath); - } - else - { - BaseFileSystem.RenameFile(srcPath, dstPath); - } - } - - public DirectoryEntryType GetEntryType(string path) - { - path = PathTools.Normalize(path); - - if (IsConcatenationFile(path)) return DirectoryEntryType.File; - - return BaseFileSystem.GetEntryType(path); - } - - public long GetFreeSpaceSize(string path) - { - return BaseFileSystem.GetFreeSpaceSize(path); - } - - public long GetTotalSpaceSize(string path) - { - return BaseFileSystem.GetTotalSpaceSize(path); - } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - return BaseFileSystem.GetFileTimeStampRaw(path); - } - - public void Commit() - { - BaseFileSystem.Commit(); - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) - { - if (queryId != QueryId.MakeConcatFile) ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationInConcatFsQueryEntry); - - SetConcatenationFileAttribute(path); - } - - private int GetSubFileCount(string dirPath) - { - int count = 0; - - while (BaseFileSystem.FileExists(GetSubFilePath(dirPath, count))) - { - count++; - } - - return count; - } - - internal static string GetSubFilePath(string dirPath, int index) - { - return $"{dirPath}/{index:D2}"; - } - - internal long GetConcatenationFileSize(string path) - { - int fileCount = GetSubFileCount(path); - long size = 0; - - for (int i = 0; i < fileCount; i++) - { - size += BaseFileSystem.GetFileSize(GetSubFilePath(path, i)); - } - - return size; - } - } -} diff --git a/src/LibHac/Fs/ContentStorage.cs b/src/LibHac/Fs/ContentStorage.cs new file mode 100644 index 00000000..c74d2763 --- /dev/null +++ b/src/LibHac/Fs/ContentStorage.cs @@ -0,0 +1,69 @@ +using System; +using LibHac.Common; +using LibHac.FsService; + +namespace LibHac.Fs +{ + public static class ContentStorage + { + private static readonly U8String ContentStorageMountNameSystem = new U8String("@SystemContent"); + private static readonly U8String ContentStorageMountNameUser = new U8String("@UserContent"); + private static readonly U8String ContentStorageMountNameSdCard = new U8String("@SdCardContent"); + + public static Result MountContentStorage(this FileSystemClient fs, ContentStorageId storageId) + { + return MountContentStorage(fs, GetContentStorageMountName(storageId), storageId); + } + + public static Result MountContentStorage(this FileSystemClient fs, U8Span mountName, ContentStorageId storageId) + { + Result rc = MountHelpers.CheckMountNameAcceptingReservedMountName(mountName); + if (rc.IsFailure()) return rc; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + rc = fsProxy.OpenContentStorageFileSystem(out IFileSystem contentFs, storageId); + if (rc.IsFailure()) return rc; + + var mountNameGenerator = new ContentStorageCommonMountNameGenerator(storageId); + + return fs.Register(mountName, contentFs, mountNameGenerator); + } + + public static U8String GetContentStorageMountName(ContentStorageId storageId) + { + switch (storageId) + { + case ContentStorageId.System: + return ContentStorageMountNameSystem; + case ContentStorageId.User: + return ContentStorageMountNameUser; + case ContentStorageId.SdCard: + return ContentStorageMountNameSdCard; + default: + throw new ArgumentOutOfRangeException(nameof(storageId), storageId, null); + } + } + + private class ContentStorageCommonMountNameGenerator : ICommonMountNameGenerator + { + private ContentStorageId StorageId { get; } + + public ContentStorageCommonMountNameGenerator(ContentStorageId storageId) + { + StorageId = storageId; + } + + public Result Generate(Span nameBuffer) + { + U8String mountName = GetContentStorageMountName(StorageId); + + int length = StringUtils.Copy(nameBuffer, mountName); + nameBuffer[length] = (byte)':'; + nameBuffer[length + 1] = 0; + + return Result.Success; + } + } + } +} diff --git a/src/LibHac/Fs/CustomStorage.cs b/src/LibHac/Fs/CustomStorage.cs new file mode 100644 index 00000000..f97727b1 --- /dev/null +++ b/src/LibHac/Fs/CustomStorage.cs @@ -0,0 +1,34 @@ +using System; +using LibHac.Common; +using LibHac.FsService; + +namespace LibHac.Fs +{ + public static class CustomStorage + { + public static Result MountCustomStorage(this FileSystemClient fs, U8Span mountName, CustomStorageId storageId) + { + Result rc = MountHelpers.CheckMountName(mountName); + if (rc.IsFailure()) return rc; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + rc = fsProxy.OpenCustomStorageFileSystem(out IFileSystem customFs, storageId); + if (rc.IsFailure()) return rc; + + return fs.Register(mountName, customFs); + } + + public static string GetCustomStorageDirectoryName(CustomStorageId storageId) + { + switch (storageId) + { + case CustomStorageId.User: + case CustomStorageId.SdCard: + return "CustomStorage0"; + default: + throw new ArgumentOutOfRangeException(nameof(storageId), storageId, null); + } + } + } +} diff --git a/src/LibHac/Fs/DirectoryEntry.cs b/src/LibHac/Fs/DirectoryEntry.cs index fbefb8e7..1d60e85d 100644 --- a/src/LibHac/Fs/DirectoryEntry.cs +++ b/src/LibHac/Fs/DirectoryEntry.cs @@ -1,8 +1,11 @@ using System; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.FsSystem; namespace LibHac.Fs { - public class DirectoryEntry + public class DirectoryEntryEx { public string Name { get; set; } public string FullPath { get; set; } @@ -10,7 +13,7 @@ namespace LibHac.Fs public DirectoryEntryType Type { get; set; } public long Size { get; set; } - public DirectoryEntry(string name, string fullPath, DirectoryEntryType type, long size) + public DirectoryEntryEx(string name, string fullPath, DirectoryEntryType type, long size) { Name = name; FullPath = PathTools.Normalize(fullPath); @@ -19,7 +22,18 @@ namespace LibHac.Fs } } - public enum DirectoryEntryType + [StructLayout(LayoutKind.Explicit)] + public struct DirectoryEntry + { + [FieldOffset(0)] private byte _name; + [FieldOffset(0x301)] public NxFileAttributes Attributes; + [FieldOffset(0x304)] public DirectoryEntryType Type; + [FieldOffset(0x308)] public long Size; + + public Span Name => SpanHelpers.CreateSpan(ref _name, PathTools.MaxPathLength + 1); + } + + public enum DirectoryEntryType : byte { Directory, File, @@ -27,7 +41,7 @@ namespace LibHac.Fs } [Flags] - public enum NxFileAttributes + public enum NxFileAttributes : byte { None = 0, Directory = 1 << 0, diff --git a/src/LibHac/Fs/Accessors/DirectoryHandle.cs b/src/LibHac/Fs/DirectoryHandle.cs similarity index 64% rename from src/LibHac/Fs/Accessors/DirectoryHandle.cs rename to src/LibHac/Fs/DirectoryHandle.cs index d4804306..1a8f606b 100644 --- a/src/LibHac/Fs/Accessors/DirectoryHandle.cs +++ b/src/LibHac/Fs/DirectoryHandle.cs @@ -1,6 +1,7 @@ using System; +using LibHac.Fs.Accessors; -namespace LibHac.Fs.Accessors +namespace LibHac.Fs { public struct DirectoryHandle : IDisposable { @@ -11,11 +12,11 @@ namespace LibHac.Fs.Accessors Directory = directory; } - public int GetId() => Directory.GetHashCode(); + public int GetId() => Directory?.GetHashCode() ?? 0; public void Dispose() { - Directory.Parent.FsManager.CloseDirectory(this); + Directory.Parent.FsClient.CloseDirectory(this); } } } diff --git a/src/LibHac/Fs/DirectorySaveDataFile.cs b/src/LibHac/Fs/DirectorySaveDataFile.cs deleted file mode 100644 index 7bda0da9..00000000 --- a/src/LibHac/Fs/DirectorySaveDataFile.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class DirectorySaveDataFile : FileBase - { - private IFile BaseFile { get; } - private DirectorySaveDataFileSystem ParentFs { get; } - private object DisposeLocker { get; } = new object(); - - public DirectorySaveDataFile(DirectorySaveDataFileSystem parentFs, IFile baseFile) - { - ParentFs = parentFs; - BaseFile = baseFile; - Mode = BaseFile.Mode; - ToDispose.Add(BaseFile); - } - - public override int Read(Span destination, long offset, ReadOption options) - { - return BaseFile.Read(destination, offset, options); - } - - public override void Write(ReadOnlySpan source, long offset, WriteOption options) - { - BaseFile.Write(source, offset, options); - } - - public override void Flush() - { - BaseFile.Flush(); - } - - public override long GetSize() - { - return BaseFile.GetSize(); - } - - public override void SetSize(long size) - { - BaseFile.SetSize(size); - } - - protected override void Dispose(bool disposing) - { - lock (DisposeLocker) - { - if (IsDisposed) return; - - base.Dispose(disposing); - - if (Mode.HasFlag(OpenMode.Write)) - { - ParentFs.NotifyCloseWritableFile(); - } - } - } - } -} diff --git a/src/LibHac/Fs/DirectorySaveDataFileSystem.cs b/src/LibHac/Fs/DirectorySaveDataFileSystem.cs deleted file mode 100644 index 3fe9f866..00000000 --- a/src/LibHac/Fs/DirectorySaveDataFileSystem.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class DirectorySaveDataFileSystem : IFileSystem - { - private const string CommittedDir = "/0/"; - private const string WorkingDir = "/1/"; - private const string SyncDir = "/_/"; - - private IFileSystem BaseFs { get; } - private object Locker { get; } = new object(); - private int OpenWritableFileCount { get; set; } - - public DirectorySaveDataFileSystem(IFileSystem baseFileSystem) - { - BaseFs = baseFileSystem; - - if (!BaseFs.DirectoryExists(WorkingDir)) - { - BaseFs.CreateDirectory(WorkingDir); - BaseFs.EnsureDirectoryExists(CommittedDir); - } - - if (BaseFs.DirectoryExists(CommittedDir)) - { - SynchronizeDirectory(WorkingDir, CommittedDir); - } - else - { - SynchronizeDirectory(SyncDir, WorkingDir); - BaseFs.RenameDirectory(SyncDir, CommittedDir); - } - } - - public void CreateDirectory(string path) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - BaseFs.CreateDirectory(fullPath); - } - } - - public void CreateFile(string path, long size, CreateFileOptions options) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - BaseFs.CreateFile(fullPath, size, options); - } - } - - public void DeleteDirectory(string path) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - BaseFs.DeleteDirectory(fullPath); - } - } - - public void DeleteDirectoryRecursively(string path) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - BaseFs.DeleteDirectoryRecursively(fullPath); - } - } - - public void CleanDirectoryRecursively(string path) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - BaseFs.CleanDirectoryRecursively(fullPath); - } - } - - public void DeleteFile(string path) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - BaseFs.DeleteFile(fullPath); - } - } - - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - return BaseFs.OpenDirectory(fullPath, mode); - } - } - - public IFile OpenFile(string path, OpenMode mode) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - IFile baseFile = BaseFs.OpenFile(fullPath, mode); - var file = new DirectorySaveDataFile(this, baseFile); - - if (mode.HasFlag(OpenMode.Write)) - { - OpenWritableFileCount++; - } - - return file; - } - } - - public void RenameDirectory(string srcPath, string dstPath) - { - string fullSrcPath = GetFullPath(PathTools.Normalize(srcPath)); - string fullDstPath = GetFullPath(PathTools.Normalize(dstPath)); - - lock (Locker) - { - BaseFs.RenameDirectory(fullSrcPath, fullDstPath); - } - } - - public void RenameFile(string srcPath, string dstPath) - { - string fullSrcPath = GetFullPath(PathTools.Normalize(srcPath)); - string fullDstPath = GetFullPath(PathTools.Normalize(dstPath)); - - lock (Locker) - { - BaseFs.RenameFile(fullSrcPath, fullDstPath); - } - } - - public DirectoryEntryType GetEntryType(string path) - { - string fullPath = GetFullPath(PathTools.Normalize(path)); - - lock (Locker) - { - return BaseFs.GetEntryType(fullPath); - } - } - - public long GetFreeSpaceSize(string path) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; - } - - public long GetTotalSpaceSize(string path) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; - } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; - } - - public void Commit() - { - if (OpenWritableFileCount > 0) - { - ThrowHelper.ThrowResult(ResultFs.WritableFileOpen, - "All files must be closed before commiting save data."); - } - - SynchronizeDirectory(SyncDir, WorkingDir); - - BaseFs.DeleteDirectoryRecursively(CommittedDir); - - BaseFs.RenameDirectory(SyncDir, CommittedDir); - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - } - - private string GetFullPath(string path) - { - return PathTools.Normalize(PathTools.Combine(WorkingDir, path)); - } - - private void SynchronizeDirectory(string dest, string src) - { - if (BaseFs.DirectoryExists(dest)) - { - BaseFs.DeleteDirectoryRecursively(dest); - } - - BaseFs.CreateDirectory(dest); - - IDirectory sourceDir = BaseFs.OpenDirectory(src, OpenDirectoryMode.All); - IDirectory destDir = BaseFs.OpenDirectory(dest, OpenDirectoryMode.All); - - sourceDir.CopyDirectory(destDir); - } - - internal void NotifyCloseWritableFile() - { - lock (Locker) - { - OpenWritableFileCount--; - } - } - } -} diff --git a/src/LibHac/Fs/ExternalKeys.cs b/src/LibHac/Fs/ExternalKeys.cs new file mode 100644 index 00000000..e0638600 --- /dev/null +++ b/src/LibHac/Fs/ExternalKeys.cs @@ -0,0 +1,65 @@ +using LibHac.Common; +using LibHac.FsService; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Spl; + +namespace LibHac.Fs +{ + public static class ExternalKeys + { + public static Result GetRightsId(this FileSystemClient fs, out RightsId rightsId, TitleId programId, + StorageId storageId) + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + return fsProxy.GetRightsId(out rightsId, programId, storageId); + } + + public static Result GetRightsId(this FileSystemClient fs, out RightsId rightsId, U8Span path) + { + rightsId = default; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + Result rc = FsPath.FromSpan(out FsPath fsPath, path); + if (rc.IsFailure()) return rc; + + return fsProxy.GetRightsIdByPath(out rightsId, ref fsPath); + } + + public static Result GetRightsId(this FileSystemClient fs, out RightsId rightsId, out byte keyGeneration, U8Span path) + { + rightsId = default; + keyGeneration = default; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + Result rc = FsPath.FromSpan(out FsPath fsPath, path); + if (rc.IsFailure()) return rc; + + return fsProxy.GetRightsIdAndKeyGenerationByPath(out rightsId, out keyGeneration, ref fsPath); + } + + public static Result RegisterExternalKey(this FileSystemClient fs, ref RightsId rightsId, ref AccessKey key) + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + return fsProxy.RegisterExternalKey(ref rightsId, ref key); + } + + public static Result UnregisterExternalKey(this FileSystemClient fs, ref RightsId rightsId) + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + return fsProxy.UnregisterExternalKey(ref rightsId); + } + + public static Result UnregisterAllExternalKey(this FileSystemClient fs) + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + return fsProxy.UnregisterAllExternalKey(); + } + } +} diff --git a/src/LibHac/Fs/FileBase.cs b/src/LibHac/Fs/FileBase.cs index aae12e8d..c8126bd4 100644 --- a/src/LibHac/Fs/FileBase.cs +++ b/src/LibHac/Fs/FileBase.cs @@ -1,106 +1,148 @@ using System; -using System.Collections.Generic; +using System.Threading; namespace LibHac.Fs { public abstract class FileBase : IFile { - protected bool IsDisposed { get; private set; } - internal List ToDispose { get; } = new List(); + // 0 = not disposed; 1 = disposed + private int _disposedState; + private bool IsDisposed => _disposedState != 0; - public abstract int Read(Span destination, long offset, ReadOption options); - public abstract void Write(ReadOnlySpan source, long offset, WriteOption options); - public abstract void Flush(); - public abstract long GetSize(); - public abstract void SetSize(long size); + protected abstract Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options); + protected abstract Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options); + protected abstract Result FlushImpl(); + protected abstract Result SetSizeImpl(long size); + protected abstract Result GetSizeImpl(out long size); - public OpenMode Mode { get; protected set; } - - protected int ValidateReadParamsAndGetSize(ReadOnlySpan span, long offset) + protected virtual Result OperateRangeImpl(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) { - if (IsDisposed) throw new ObjectDisposedException(null); - - if ((Mode & OpenMode.Read) == 0) ThrowHelper.ThrowResult(ResultFs.InvalidOpenModeForRead, "File does not allow reading."); - if (span == null) throw new ArgumentNullException(nameof(span)); - if (offset < 0) ThrowHelper.ThrowResult(ResultFs.ValueOutOfRange, "Offset must be non-negative."); - - long fileSize = GetSize(); - int size = span.Length; - - if (offset > fileSize) ThrowHelper.ThrowResult(ResultFs.ValueOutOfRange, "Offset must be less than the file size."); - - return (int)Math.Min(fileSize - offset, size); + return ResultFs.NotImplemented.Log(); } - protected void ValidateWriteParams(ReadOnlySpan span, long offset) + public Result Read(out long bytesRead, long offset, Span destination, ReadOption options) { - if (IsDisposed) throw new ObjectDisposedException(null); + bytesRead = default; - if ((Mode & OpenMode.Write) == 0) ThrowHelper.ThrowResult(ResultFs.InvalidOpenModeForWrite, "File does not allow writing."); + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); - if (span == null) throw new ArgumentNullException(nameof(span)); - if (offset < 0) ThrowHelper.ThrowResult(ResultFs.ValueOutOfRange, "Offset must be non-negative."); + if (destination.Length == 0) return Result.Success; + if (offset < 0) return ResultFs.ValueOutOfRange.Log(); - long fileSize = GetSize(); - int size = span.Length; + return ReadImpl(out bytesRead, offset, destination, options); + } - if (offset + size > fileSize) + public Result Write(long offset, ReadOnlySpan source, WriteOption options) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + if (source.Length == 0) { - if ((Mode & OpenMode.Append) == 0) + if (options.HasFlag(WriteOption.Flush)) { - ThrowHelper.ThrowResult(ResultFs.AllowAppendRequiredForImplicitExtension); + return Flush(); } - SetSize(offset + size); + return Result.Success; } + + if (offset < 0) return ResultFs.ValueOutOfRange.Log(); + + return WriteImpl(offset, source, options); + } + + public Result Flush() + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return FlushImpl(); + } + + public Result SetSize(long size) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + if (size < 0) return ResultFs.ValueOutOfRange.Log(); + + return SetSizeImpl(size); + } + + public Result GetSize(out long size) + { + if (IsDisposed) + { + size = default; + return ResultFs.PreconditionViolation.Log(); + } + + return GetSizeImpl(out size); + } + + public Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return OperateRange(outBuffer, operationId, offset, size, inBuffer); } public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + // Make sure Dispose is only called once + if (Interlocked.CompareExchange(ref _disposedState, 1, 0) == 0) + { + Dispose(true); + GC.SuppressFinalize(this); + } } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) { } + + protected Result ValidateReadParams(out long bytesToRead, long offset, int size, OpenMode openMode) { - if (IsDisposed) return; + bytesToRead = default; - if (disposing) + if (!openMode.HasFlag(OpenMode.Read)) { - Flush(); + return ResultFs.InvalidOpenModeForRead.Log(); + } - foreach (IDisposable item in ToDispose) + Result rc = GetSize(out long fileSize); + if (rc.IsFailure()) return rc; + + if (offset > fileSize) + { + return ResultFs.ValueOutOfRange.Log(); + } + + bytesToRead = Math.Min(fileSize - offset, size); + + return Result.Success; + } + + protected Result ValidateWriteParams(long offset, int size, OpenMode openMode, out bool isResizeNeeded) + { + isResizeNeeded = false; + + if (!openMode.HasFlag(OpenMode.Write)) + { + return ResultFs.InvalidOpenModeForWrite.Log(); + } + + Result rc = GetSize(out long fileSize); + if (rc.IsFailure()) return rc; + + if (offset + size > fileSize) + { + isResizeNeeded = true; + + if (!openMode.HasFlag(OpenMode.AllowAppend)) { - item?.Dispose(); + return ResultFs.FileExtensionWithoutOpenModeAllowAppend.Log(); } } - IsDisposed = true; + return Result.Success; } } - - /// - /// Specifies which operations are available on an . - /// - [Flags] - public enum OpenMode - { - Read = 1, - Write = 2, - Append = 4, - ReadWrite = Read | Write - } - - [Flags] - public enum ReadOption - { - None = 0 - } - - [Flags] - public enum WriteOption - { - None = 0, - Flush = 1 - } } diff --git a/src/LibHac/Fs/Accessors/FileHandle.cs b/src/LibHac/Fs/FileHandle.cs similarity index 63% rename from src/LibHac/Fs/Accessors/FileHandle.cs rename to src/LibHac/Fs/FileHandle.cs index da454f4d..eb927d5b 100644 --- a/src/LibHac/Fs/Accessors/FileHandle.cs +++ b/src/LibHac/Fs/FileHandle.cs @@ -1,6 +1,7 @@ using System; +using LibHac.Fs.Accessors; -namespace LibHac.Fs.Accessors +namespace LibHac.Fs { public struct FileHandle : IDisposable { @@ -11,11 +12,11 @@ namespace LibHac.Fs.Accessors File = file; } - public int GetId() => File.GetHashCode(); + public int GetId() => File?.GetHashCode() ?? 0; public void Dispose() { - File.Parent.FsManager.CloseFile(this); + File.Parent.FsClient.CloseFile(this); } } } diff --git a/src/LibHac/Fs/FileHandleStorage.cs b/src/LibHac/Fs/FileHandleStorage.cs new file mode 100644 index 00000000..c9389681 --- /dev/null +++ b/src/LibHac/Fs/FileHandleStorage.cs @@ -0,0 +1,96 @@ +using System; + +namespace LibHac.Fs +{ + public class FileHandleStorage : StorageBase + { + private const long InvalidSize = -1; + private readonly object _locker = new object(); + + private FileSystemClient FsClient { get; } + private FileHandle Handle { get; } + private long FileSize { get; set; } = InvalidSize; + private bool CloseHandle { get; } + + public FileHandleStorage(FileHandle handle) : this(handle, false) { } + + public FileHandleStorage(FileHandle handle, bool closeHandleOnDispose) + { + Handle = handle; + CloseHandle = closeHandleOnDispose; + FsClient = Handle.File.Parent.FsClient; + } + + protected override Result ReadImpl(long offset, Span destination) + { + lock (_locker) + { + if (destination.Length == 0) return Result.Success; + + Result rc = UpdateSize(); + if (rc.IsFailure()) return rc; + + if (!IsRangeValid(offset, destination.Length, FileSize)) return ResultFs.ValueOutOfRange.Log(); + + return FsClient.ReadFile(Handle, offset, destination); + } + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source) + { + lock (_locker) + { + if (source.Length == 0) return Result.Success; + + Result rc = UpdateSize(); + if (rc.IsFailure()) return rc; + + if (!IsRangeValid(offset, source.Length, FileSize)) return ResultFs.ValueOutOfRange.Log(); + + return FsClient.WriteFile(Handle, offset, source); + } + } + + protected override Result FlushImpl() + { + return FsClient.FlushFile(Handle); + } + + protected override Result SetSizeImpl(long size) + { + FileSize = InvalidSize; + + return FsClient.SetFileSize(Handle, size); + } + + protected override Result GetSizeImpl(out long size) + { + size = default; + + Result rc = UpdateSize(); + if (rc.IsFailure()) return rc; + + size = FileSize; + return Result.Success; + } + + private Result UpdateSize() + { + if (FileSize != InvalidSize) return Result.Success; + + Result rc = FsClient.GetFileSize(out long fileSize, Handle); + if (rc.IsFailure()) return rc; + + FileSize = fileSize; + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (CloseHandle) + { + FsClient.CloseFile(Handle); + } + } + } +} diff --git a/src/LibHac/Fs/FileStorage.cs b/src/LibHac/Fs/FileStorage.cs deleted file mode 100644 index 80c8e6bc..00000000 --- a/src/LibHac/Fs/FileStorage.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class FileStorage : StorageBase - { - private IFile BaseFile { get; } - - public FileStorage(IFile baseFile) - { - BaseFile = baseFile; - } - - protected override void ReadImpl(Span destination, long offset) - { - BaseFile.Read(destination, offset); - } - - protected override void WriteImpl(ReadOnlySpan source, long offset) - { - BaseFile.Write(source, offset); - } - - public override void Flush() - { - BaseFile.Flush(); - } - - public override long GetSize() => BaseFile.GetSize(); - - public override void SetSize(long size) - { - BaseFile.SetSize(size); - } - } -} diff --git a/src/LibHac/Fs/FileSystemBase.cs b/src/LibHac/Fs/FileSystemBase.cs new file mode 100644 index 00000000..889cad2e --- /dev/null +++ b/src/LibHac/Fs/FileSystemBase.cs @@ -0,0 +1,196 @@ +using System; +using System.Threading; + +namespace LibHac.Fs +{ + public abstract class FileSystemBase : IFileSystem + { + // 0 = not disposed; 1 = disposed + private int _disposedState; + private bool IsDisposed => _disposedState != 0; + + protected abstract Result CreateDirectoryImpl(string path); + protected abstract Result CreateFileImpl(string path, long size, CreateFileOptions options); + protected abstract Result DeleteDirectoryImpl(string path); + protected abstract Result DeleteDirectoryRecursivelyImpl(string path); + protected abstract Result CleanDirectoryRecursivelyImpl(string path); + protected abstract Result DeleteFileImpl(string path); + protected abstract Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode); + protected abstract Result OpenFileImpl(out IFile file, string path, OpenMode mode); + protected abstract Result RenameDirectoryImpl(string oldPath, string newPath); + protected abstract Result RenameFileImpl(string oldPath, string newPath); + protected abstract Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path); + protected abstract Result CommitImpl(); + + protected virtual Result GetFreeSpaceSizeImpl(out long freeSpace, string path) + { + freeSpace = default; + return ResultFs.NotImplemented.Log(); + } + + protected virtual Result GetTotalSpaceSizeImpl(out long totalSpace, string path) + { + totalSpace = default; + return ResultFs.NotImplemented.Log(); + } + + protected virtual Result GetFileTimeStampRawImpl(out FileTimeStampRaw timeStamp, string path) + { + timeStamp = default; + return ResultFs.NotImplemented.Log(); + } + + protected virtual Result QueryEntryImpl(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, string path) + { + return ResultFs.NotImplemented.Log(); + } + + public Result CreateDirectory(string path) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return CreateDirectoryImpl(path); + } + + public Result CreateFile(string path, long size, CreateFileOptions options) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return CreateFileImpl(path, size, options); + } + + public Result DeleteDirectory(string path) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return DeleteDirectoryImpl(path); + } + + public Result DeleteDirectoryRecursively(string path) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return DeleteDirectoryRecursivelyImpl(path); + } + + public Result CleanDirectoryRecursively(string path) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return CleanDirectoryRecursivelyImpl(path); + } + + public Result DeleteFile(string path) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return DeleteFileImpl(path); + } + + public Result OpenDirectory(out IDirectory directory, string path, OpenDirectoryMode mode) + { + if (IsDisposed) + { + directory = default; + return ResultFs.PreconditionViolation.Log(); + } + + return OpenDirectoryImpl(out directory, path, mode); + } + + public Result OpenFile(out IFile file, string path, OpenMode mode) + { + if (IsDisposed) + { + file = default; + return ResultFs.PreconditionViolation.Log(); + } + + return OpenFileImpl(out file, path, mode); + } + + public Result RenameDirectory(string oldPath, string newPath) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return RenameDirectoryImpl(oldPath, newPath); + } + + public Result RenameFile(string oldPath, string newPath) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return RenameFileImpl(oldPath, newPath); + } + + public Result GetEntryType(out DirectoryEntryType entryType, string path) + { + if (IsDisposed) + { + entryType = default; + return ResultFs.PreconditionViolation.Log(); + } + + return GetEntryTypeImpl(out entryType, path); + } + + public Result GetFreeSpaceSize(out long freeSpace, string path) + { + if (IsDisposed) + { + freeSpace = default; + return ResultFs.PreconditionViolation.Log(); + } + + return GetFreeSpaceSizeImpl(out freeSpace, path); + } + + public Result GetTotalSpaceSize(out long totalSpace, string path) + { + if (IsDisposed) + { + totalSpace = default; + return ResultFs.PreconditionViolation.Log(); + } + + return GetTotalSpaceSizeImpl(out totalSpace, path); + } + + public Result GetFileTimeStampRaw(out FileTimeStampRaw timeStamp, string path) + { + if (IsDisposed) + { + timeStamp = default; + return ResultFs.PreconditionViolation.Log(); + } + + return GetFileTimeStampRawImpl(out timeStamp, path); + } + + public Result Commit() + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return CommitImpl(); + } + + public Result QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, string path) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return QueryEntryImpl(outBuffer, inBuffer, queryId, path); + } + + public void Dispose() + { + // Make sure Dispose is only called once + if (Interlocked.CompareExchange(ref _disposedState, 1, 0) == 0) + { + Dispose(true); + GC.SuppressFinalize(this); + } + } + + protected virtual void Dispose(bool disposing) { } + } +} diff --git a/src/LibHac/Fs/FileSystemClient.AccessLog.cs b/src/LibHac/Fs/FileSystemClient.AccessLog.cs new file mode 100644 index 00000000..f7e89383 --- /dev/null +++ b/src/LibHac/Fs/FileSystemClient.AccessLog.cs @@ -0,0 +1,220 @@ +using System; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Fs.Accessors; +using LibHac.FsService; + +namespace LibHac.Fs +{ + public partial class FileSystemClient + { + private GlobalAccessLogMode GlobalAccessLogMode { get; set; } + private LocalAccessLogMode LocalAccessLogMode { get; set; } + private bool AccessLogInitialized { get; set; } + + private readonly object _accessLogInitLocker = new object(); + + public Result GetGlobalAccessLogMode(out GlobalAccessLogMode mode) + { + if (HasFileSystemServer()) + { + IFileSystemProxy fsProxy = GetFileSystemProxyServiceObject(); + + return fsProxy.GetGlobalAccessLogMode(out mode); + } + + mode = GlobalAccessLogMode; + return Result.Success; + } + + public Result SetGlobalAccessLogMode(GlobalAccessLogMode mode) + { + if (HasFileSystemServer()) + { + IFileSystemProxy fsProxy = GetFileSystemProxyServiceObject(); + + return fsProxy.SetGlobalAccessLogMode(mode); + } + + GlobalAccessLogMode = mode; + return Result.Success; + } + + public void SetLocalAccessLogMode(LocalAccessLogMode mode) + { + LocalAccessLogMode = mode; + } + + public void SetAccessLogObject(IAccessLog accessLog) + { + AccessLog = accessLog; + } + + internal bool IsEnabledAccessLog(LocalAccessLogMode mode) + { + if ((LocalAccessLogMode & mode) == 0) + { + return false; + } + + if (AccessLogInitialized) + { + return GlobalAccessLogMode != GlobalAccessLogMode.None; + } + + lock (_accessLogInitLocker) + { + if (!AccessLogInitialized) + { + if (HasFileSystemServer()) + { + IFileSystemProxy fsProxy = GetFileSystemProxyServiceObject(); + + Result rc = fsProxy.GetGlobalAccessLogMode(out GlobalAccessLogMode globalMode); + GlobalAccessLogMode = globalMode; + + if (rc.IsFailure()) + { + throw new LibHacException("Abort"); + } + } + else + { + GlobalAccessLogMode = GlobalAccessLogMode.Log; + } + + if (GlobalAccessLogMode != GlobalAccessLogMode.None) + { + InitAccessLog(); + } + + AccessLogInitialized = true; + } + } + + return GlobalAccessLogMode != GlobalAccessLogMode.None; + } + + private void InitAccessLog() + { + + } + + internal bool IsEnabledAccessLog() + { + return IsEnabledAccessLog(LocalAccessLogMode.All); + } + + internal bool IsEnabledFileSystemAccessorAccessLog(string mountName) + { + if (MountTable.Find(mountName, out FileSystemAccessor accessor).IsFailure()) + { + return true; + } + + return accessor.IsAccessLogEnabled; + } + + internal bool IsEnabledHandleAccessLog(FileHandle handle) + { + return handle.File.Parent.IsAccessLogEnabled; + } + + internal bool IsEnabledHandleAccessLog(DirectoryHandle handle) + { + return handle.Directory.Parent.IsAccessLogEnabled; + } + + internal void OutputAccessLog(Result result, TimeSpan startTime, TimeSpan endTime, string message, [CallerMemberName] string caller = "") + { + OutputAccessLogImpl(result, startTime, endTime, 0, message, caller); + } + + internal void OutputAccessLog(Result result, TimeSpan startTime, TimeSpan endTime, FileHandle handle, string message, [CallerMemberName] string caller = "") + { + OutputAccessLogImpl(result, startTime, endTime, handle.GetId(), message, caller); + } + + internal void OutputAccessLog(Result result, TimeSpan startTime, TimeSpan endTime, DirectoryHandle handle, string message, [CallerMemberName] string caller = "") + { + OutputAccessLogImpl(result, startTime, endTime, handle.GetId(), message, caller); + } + + internal void OutputAccessLogImpl(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, + string message, [CallerMemberName] string caller = "") + { + if (GlobalAccessLogMode.HasFlag(GlobalAccessLogMode.Log)) + { + AccessLog?.Log(result, startTime, endTime, handleId, message, caller); + } + + if (GlobalAccessLogMode.HasFlag(GlobalAccessLogMode.SdCard)) + { + string logString = AccessLogHelpers.BuildDefaultLogLine(result, startTime, endTime, handleId, message, caller); + + IFileSystemProxy fsProxy = GetFileSystemProxyServiceObject(); + fsProxy.OutputAccessLogToSdCard(logString.ToU8Span()); + } + } + + public Result RunOperationWithAccessLog(LocalAccessLogMode logType, Func operation, + Func textGenerator, [CallerMemberName] string caller = "") + { + Result rc; + + if (IsEnabledAccessLog(logType)) + { + TimeSpan startTime = Time.GetCurrent(); + rc = operation(); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, textGenerator(), caller); + } + else + { + rc = operation(); + } + + return rc; + } + + public Result RunOperationWithAccessLog(LocalAccessLogMode logType, FileHandle handle, Func operation, + Func textGenerator, [CallerMemberName] string caller = "") + { + Result rc; + + if (IsEnabledAccessLog(logType) && handle.File.Parent.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = operation(); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, handle, textGenerator(), caller); + } + else + { + rc = operation(); + } + + return rc; + } + } + + [Flags] + public enum LocalAccessLogMode + { + None = 0, + Application = 1 << 0, + System = 1 << 1, + All = Application | System + } + + [Flags] + public enum GlobalAccessLogMode + { + None = 0, + Log = 1 << 0, + SdCard = 1 << 1, + All = Log | SdCard + } +} diff --git a/src/LibHac/Fs/FileSystemClient.Directory.cs b/src/LibHac/Fs/FileSystemClient.Directory.cs new file mode 100644 index 00000000..499b7150 --- /dev/null +++ b/src/LibHac/Fs/FileSystemClient.Directory.cs @@ -0,0 +1,48 @@ +using System; + +namespace LibHac.Fs +{ + public partial class FileSystemClient + { + public Result GetDirectoryEntryCount(out long count, DirectoryHandle handle) + { + return handle.Directory.GetEntryCount(out count); + } + + public Result ReadDirectory(out long entriesRead, Span entryBuffer, DirectoryHandle handle) + { + Result rc; + + if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) + { + TimeSpan startTime = Time.GetCurrent(); + rc = handle.Directory.Read(out entriesRead, entryBuffer); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, handle, string.Empty); + } + else + { + rc = handle.Directory.Read(out entriesRead, entryBuffer); + } + + return rc; + } + + public void CloseDirectory(DirectoryHandle handle) + { + if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) + { + TimeSpan startTime = Time.GetCurrent(); + handle.Directory.Dispose(); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(Result.Success, startTime, endTime, handle, string.Empty); + } + else + { + handle.Directory.Dispose(); + } + } + } +} diff --git a/src/LibHac/Fs/FileSystemClient.File.cs b/src/LibHac/Fs/FileSystemClient.File.cs new file mode 100644 index 00000000..aab0c161 --- /dev/null +++ b/src/LibHac/Fs/FileSystemClient.File.cs @@ -0,0 +1,109 @@ +using System; + +namespace LibHac.Fs +{ + public partial class FileSystemClient + { + public Result ReadFile(FileHandle handle, long offset, Span destination) + { + return ReadFile(handle, offset, destination, ReadOption.None); + } + + public Result ReadFile(FileHandle handle, long offset, Span destination, ReadOption option) + { + Result rc = ReadFile(out long bytesRead, handle, offset, destination, option); + if (rc.IsFailure()) return rc; + + if (bytesRead == destination.Length) return Result.Success; + + return ResultFs.ValueOutOfRange.Log(); + } + + public Result ReadFile(out long bytesRead, FileHandle handle, long offset, Span destination) + { + return ReadFile(out bytesRead, handle, offset, destination, ReadOption.None); + } + + public Result ReadFile(out long bytesRead, FileHandle handle, long offset, Span destination, ReadOption option) + { + Result rc; + + if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) + { + TimeSpan startTime = Time.GetCurrent(); + rc = handle.File.Read(out bytesRead, offset, destination, option); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, handle, $", offset: {offset}, size: {destination.Length}"); + } + else + { + rc = handle.File.Read(out bytesRead, offset, destination, option); + } + + return rc; + } + + public Result WriteFile(FileHandle handle, long offset, ReadOnlySpan source) + { + return WriteFile(handle, offset, source, WriteOption.None); + } + + public Result WriteFile(FileHandle handle, long offset, ReadOnlySpan source, WriteOption option) + { + Result rc; + + if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) + { + TimeSpan startTime = Time.GetCurrent(); + rc = handle.File.Write(offset, source, option); + TimeSpan endTime = Time.GetCurrent(); + + string optionString = (option & WriteOption.Flush) == 0 ? "" : $", write_option: {option}"; + + OutputAccessLog(rc, startTime, endTime, handle, $", offset: {offset}, size: {source.Length}{optionString}"); + } + else + { + rc = handle.File.Write(offset, source, option); + } + + return rc; + } + + public Result FlushFile(FileHandle handle) + { + return RunOperationWithAccessLog(LocalAccessLogMode.All, handle, + () => handle.File.Flush(), + () => string.Empty); + } + + public Result GetFileSize(out long fileSize, FileHandle handle) + { + return handle.File.GetSize(out fileSize); + } + + public Result SetFileSize(FileHandle handle, long size) + { + return RunOperationWithAccessLog(LocalAccessLogMode.All, handle, + () => handle.File.SetSize(size), + () => $", size: {size}"); + } + + public OpenMode GetFileOpenMode(FileHandle handle) + { + return handle.File.OpenMode; + } + + public void CloseFile(FileHandle handle) + { + RunOperationWithAccessLog(LocalAccessLogMode.All, handle, + () => + { + handle.File.Dispose(); + return Result.Success; + }, + () => string.Empty); + } + } +} diff --git a/src/LibHac/Fs/FileSystemClient.FileSystem.cs b/src/LibHac/Fs/FileSystemClient.FileSystem.cs new file mode 100644 index 00000000..7ed7e6a9 --- /dev/null +++ b/src/LibHac/Fs/FileSystemClient.FileSystem.cs @@ -0,0 +1,321 @@ +using System; +using LibHac.Fs.Accessors; + +namespace LibHac.Fs +{ + public partial class FileSystemClient + { + public Result CreateDirectory(string path) + { + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.CreateDirectory(subPath.ToString()); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{path}\""); + } + else + { + rc = fileSystem.CreateDirectory(subPath.ToString()); + } + + return rc; + } + + public Result CreateFile(string path, long size) + { + return CreateFile(path, size, CreateFileOptions.None); + } + + public Result CreateFile(string path, long size, CreateFileOptions options) + { + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.CreateFile(subPath.ToString(), size, options); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{path}\", size: {size}"); + } + else + { + rc = fileSystem.CreateFile(subPath.ToString(), size, options); + } + + return rc; + } + + public Result DeleteDirectory(string path) + { + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.DeleteDirectory(subPath.ToString()); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{path}\""); + } + else + { + rc = fileSystem.DeleteDirectory(subPath.ToString()); + } + + return rc; + } + + public Result DeleteDirectoryRecursively(string path) + { + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.DeleteDirectoryRecursively(subPath.ToString()); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{path}\""); + } + else + { + rc = fileSystem.DeleteDirectoryRecursively(subPath.ToString()); + } + + return rc; + } + + public Result CleanDirectoryRecursively(string path) + { + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.CleanDirectoryRecursively(subPath.ToString()); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{path}\""); + } + else + { + rc = fileSystem.CleanDirectoryRecursively(subPath.ToString()); + } + + return rc; + } + + public Result DeleteFile(string path) + { + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.DeleteFile(subPath.ToString()); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{path}\""); + } + else + { + rc = fileSystem.DeleteFile(subPath.ToString()); + } + + return rc; + } + + public Result RenameDirectory(string oldPath, string newPath) + { + Result rc = FindFileSystem(oldPath.AsSpan(), out FileSystemAccessor oldFileSystem, out ReadOnlySpan oldSubPath); + if (rc.IsFailure()) return rc; + + rc = FindFileSystem(newPath.AsSpan(), out FileSystemAccessor newFileSystem, out ReadOnlySpan newSubPath); + if (rc.IsFailure()) return rc; + + if (oldFileSystem != newFileSystem) + { + return ResultFs.DifferentDestFileSystem.Log(); + } + + if (IsEnabledAccessLog() && oldFileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = oldFileSystem.RenameDirectory(oldSubPath.ToString(), newSubPath.ToString()); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{oldPath}\", new_path: \"{newPath}\""); + } + else + { + rc = oldFileSystem.RenameDirectory(oldSubPath.ToString(), newSubPath.ToString()); + } + + return rc; + } + + public Result RenameFile(string oldPath, string newPath) + { + Result rc = FindFileSystem(oldPath.AsSpan(), out FileSystemAccessor oldFileSystem, out ReadOnlySpan oldSubPath); + if (rc.IsFailure()) return rc; + + rc = FindFileSystem(newPath.AsSpan(), out FileSystemAccessor newFileSystem, out ReadOnlySpan newSubPath); + if (rc.IsFailure()) return rc; + + if (oldFileSystem != newFileSystem) + { + return ResultFs.DifferentDestFileSystem.Log(); + } + + if (IsEnabledAccessLog() && oldFileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = oldFileSystem.RenameFile(oldSubPath.ToString(), newSubPath.ToString()); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{oldPath}\", new_path: \"{newPath}\""); + } + else + { + rc = oldFileSystem.RenameFile(oldSubPath.ToString(), newSubPath.ToString()); + } + + return rc; + } + + public Result GetEntryType(out DirectoryEntryType type, string path) + { + type = default; + + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.GetEntryType(out type, subPath.ToString()); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", path: \"{path}\""); + } + else + { + rc = fileSystem.GetEntryType(out type, subPath.ToString()); + } + + return rc; + } + + public Result OpenFile(out FileHandle handle, string path, OpenMode mode) + { + handle = default; + + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.OpenFile(out FileAccessor file, subPath.ToString(), mode); + handle = new FileHandle(file); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, handle, $", path: \"{path}\", open_mode: {mode}"); + } + else + { + rc = fileSystem.OpenFile(out FileAccessor file, subPath.ToString(), mode); + handle = new FileHandle(file); + } + + return rc; + } + + public Result OpenDirectory(out DirectoryHandle handle, string path, OpenDirectoryMode mode) + { + handle = default; + + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.OpenDirectory(out DirectoryAccessor dir, subPath.ToString(), mode); + handle = new DirectoryHandle(dir); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, handle, $", path: \"{path}\", open_mode: {mode}"); + } + else + { + rc = fileSystem.OpenDirectory(out DirectoryAccessor dir, subPath.ToString(), mode); + handle = new DirectoryHandle(dir); + } + + return rc; + } + + public Result GetFreeSpaceSize(out long freeSpace, string path) + { + freeSpace = default; + + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + return fileSystem.GetFreeSpaceSize(out freeSpace, subPath.ToString()); + } + + public Result GetTotalSpaceSize(out long totalSpace, string path) + { + totalSpace = default; + + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + return fileSystem.GetTotalSpaceSize(out totalSpace, subPath.ToString()); + } + + public Result GetFileTimeStamp(out FileTimeStampRaw timeStamp, string path) + { + timeStamp = default; + + Result rc = FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath); + if (rc.IsFailure()) return rc; + + return fileSystem.GetFileTimeStampRaw(out timeStamp, subPath.ToString()); + } + + public Result Commit(string mountName) + { + Result rc = MountTable.Find(mountName, out FileSystemAccessor fileSystem); + if (rc.IsFailure()) return rc; + + if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) + { + TimeSpan startTime = Time.GetCurrent(); + rc = fileSystem.Commit(); + TimeSpan endTime = Time.GetCurrent(); + + OutputAccessLog(rc, startTime, endTime, $", name: \"{mountName}\""); + } + else + { + rc = fileSystem.Commit(); + } + + return rc; + } + } +} diff --git a/src/LibHac/Fs/FileSystemClient.cs b/src/LibHac/Fs/FileSystemClient.cs new file mode 100644 index 00000000..d4957282 --- /dev/null +++ b/src/LibHac/Fs/FileSystemClient.cs @@ -0,0 +1,142 @@ +using System; +using LibHac.Common; +using LibHac.Fs.Accessors; +using LibHac.FsService; +using LibHac.FsSystem; + +namespace LibHac.Fs +{ + public partial class FileSystemClient + { + private FileSystemServer FsSrv { get; } + private IFileSystemProxy FsProxy { get; set; } + + private readonly object _fspInitLocker = new object(); + + internal ITimeSpanGenerator Time { get; } + private IAccessLog AccessLog { get; set; } + + internal MountTable MountTable { get; } = new MountTable(); + + public FileSystemClient(ITimeSpanGenerator timer) + { + Time = timer ?? new StopWatchTimeSpanGenerator(); + } + + public FileSystemClient(FileSystemServer fsServer, ITimeSpanGenerator timer) + { + FsSrv = fsServer; + Time = timer ?? new StopWatchTimeSpanGenerator(); + } + + public bool HasFileSystemServer() + { + return FsSrv != null; + } + + public IFileSystemProxy GetFileSystemProxyServiceObject() + { + if (FsProxy != null) return FsProxy; + + lock (_fspInitLocker) + { + if (FsProxy != null) return FsProxy; + + if (!HasFileSystemServer()) + { + throw new InvalidOperationException("Client was not initialized with a server object."); + } + + FsProxy = FsSrv.CreateFileSystemProxyService(); + + return FsProxy; + } + } + + public Result Register(U8Span mountName, IFileSystem fileSystem) + { + return Register(mountName, fileSystem, null); + } + + public Result Register(U8Span mountName, IFileSystem fileSystem, ICommonMountNameGenerator nameGenerator) + { + var accessor = new FileSystemAccessor(mountName.ToString(), fileSystem, this, nameGenerator); + + Result rc = MountTable.Mount(accessor); + if (rc.IsFailure()) return rc; + + accessor.IsAccessLogEnabled = IsEnabledAccessLog(); + return Result.Success; + } + + public void Unmount(string mountName) + { + Result rc; + + if (IsEnabledAccessLog() && IsEnabledFileSystemAccessorAccessLog(mountName)) + { + TimeSpan startTime = Time.GetCurrent(); + + rc = MountTable.Unmount(mountName); + + TimeSpan endTime = Time.GetCurrent(); + OutputAccessLog(rc, startTime, endTime, $", name: \"{mountName}\""); + } + else + { + rc = MountTable.Unmount(mountName); + } + + rc.ThrowIfFailure(); + } + + internal Result FindFileSystem(ReadOnlySpan path, out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) + { + fileSystem = default; + + Result rc = GetMountName(path, out ReadOnlySpan mountName, out subPath); + if (rc.IsFailure()) return rc; + + rc = MountTable.Find(mountName.ToString(), out fileSystem); + if (rc.IsFailure()) return rc; + + return Result.Success; + } + + internal static Result GetMountName(ReadOnlySpan path, out ReadOnlySpan mountName, out ReadOnlySpan subPath) + { + int mountLen = 0; + int maxMountLen = Math.Min(path.Length, PathTools.MountNameLength); + + for (int i = 0; i < maxMountLen; i++) + { + if (path[i] == PathTools.MountSeparator) + { + mountLen = i; + break; + } + } + + if (mountLen == 0) + { + mountName = default; + subPath = default; + + return ResultFs.InvalidMountName; + } + + mountName = path.Slice(0, mountLen); + + if (mountLen + 1 < path.Length) + { + subPath = path.Slice(mountLen + 1); + } + else + { + subPath = default; + } + + return Result.Success; + } + } +} diff --git a/src/LibHac/Fs/FileSystemClientUtils.cs b/src/LibHac/Fs/FileSystemClientUtils.cs new file mode 100644 index 00000000..0165f354 --- /dev/null +++ b/src/LibHac/Fs/FileSystemClientUtils.cs @@ -0,0 +1,222 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using LibHac.Common; +using LibHac.Fs.Accessors; +using LibHac.FsSystem; + +namespace LibHac.Fs +{ + public static class FileSystemClientUtils + { + public static Result CopyDirectory(this FileSystemClient fs, string sourcePath, string destPath, + CreateFileOptions options = CreateFileOptions.None, IProgressReport logger = null) + { + Result rc = fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath, OpenDirectoryMode.All); + if (rc.IsFailure()) return rc; + + using (sourceHandle) + { + foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) + { + string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); + string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); + + if (entry.Type == DirectoryEntryType.Directory) + { + fs.EnsureDirectoryExists(subDstPath); + + rc = fs.CopyDirectory(subSrcPath, subDstPath, options, logger); + if (rc.IsFailure()) return rc; + } + + if (entry.Type == DirectoryEntryType.File) + { + logger?.LogMessage(subSrcPath); + fs.CreateOrOverwriteFile(subDstPath, entry.Size, options); + + rc = fs.CopyFile(subSrcPath, subDstPath, logger); + if (rc.IsFailure()) return rc; + } + } + } + + return Result.Success; + } + + public static Result CopyFile(this FileSystemClient fs, string sourcePath, string destPath, IProgressReport logger = null) + { + Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath, OpenMode.Read); + if (rc.IsFailure()) return rc; + + using (sourceHandle) + { + rc = fs.OpenFile(out FileHandle destHandle, destPath, OpenMode.Write | OpenMode.AllowAppend); + if (rc.IsFailure()) return rc; + + using (destHandle) + { + const int maxBufferSize = 0x10000; + + rc = fs.GetFileSize(out long fileSize, sourceHandle); + if (rc.IsFailure()) return rc; + + int bufferSize = (int)Math.Min(maxBufferSize, fileSize); + + logger?.SetTotal(fileSize); + + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try + { + for (long offset = 0; offset < fileSize; offset += bufferSize) + { + int toRead = (int)Math.Min(fileSize - offset, bufferSize); + Span buf = buffer.AsSpan(0, toRead); + + rc = fs.ReadFile(out long _, sourceHandle, offset, buf); + if (rc.IsFailure()) return rc; + + rc = fs.WriteFile(destHandle, offset, buf); + if (rc.IsFailure()) return rc; + + logger?.ReportAdd(toRead); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + logger?.SetTotal(0); + } + + rc = fs.FlushFile(destHandle); + if (rc.IsFailure()) return rc; + } + } + + return Result.Success; + } + + public static IEnumerable EnumerateEntries(this FileSystemClient fs, string path) + { + return fs.EnumerateEntries(path, "*"); + } + + public static IEnumerable EnumerateEntries(this FileSystemClient fs, string path, string searchPattern) + { + return fs.EnumerateEntries(path, searchPattern, SearchOptions.RecurseSubdirectories); + } + + public static IEnumerable EnumerateEntries(this FileSystemClient fs, string path, string searchPattern, SearchOptions searchOptions) + { + bool ignoreCase = searchOptions.HasFlag(SearchOptions.CaseInsensitive); + bool recurse = searchOptions.HasFlag(SearchOptions.RecurseSubdirectories); + + DirectoryEntry dirEntry = default; + fs.OpenDirectory(out DirectoryHandle sourceHandle, path, OpenDirectoryMode.All).ThrowIfFailure(); + + using (sourceHandle) + { + while (true) + { + fs.ReadDirectory(out long entriesRead, SpanHelpers.AsSpan(ref dirEntry), sourceHandle); + if (entriesRead == 0) break; + + DirectoryEntryEx entry = FileSystemExtensions.GetDirectoryEntryEx(ref dirEntry, path); + + if (PathTools.MatchesPattern(searchPattern, entry.Name, ignoreCase)) + { + yield return entry; + } + + if (entry.Type != DirectoryEntryType.Directory || !recurse) continue; + + IEnumerable subEntries = + fs.EnumerateEntries(PathTools.Combine(path, entry.Name), searchPattern, searchOptions); + + foreach (DirectoryEntryEx subEntry in subEntries) + { + yield return subEntry; + } + } + } + } + + public static bool DirectoryExists(this FileSystemClient fs, string path) + { + Result rc = fs.GetEntryType(out DirectoryEntryType type, path); + + return (rc.IsSuccess() && type == DirectoryEntryType.Directory); + } + + public static bool FileExists(this FileSystemClient fs, string path) + { + Result rc = fs.GetEntryType(out DirectoryEntryType type, path); + + return (rc.IsSuccess() && type == DirectoryEntryType.File); + } + + public static void EnsureDirectoryExists(this FileSystemClient fs, string path) + { + path = PathTools.Normalize(path); + if (fs.DirectoryExists(path)) return; + + PathTools.GetMountNameLength(path, out int mountNameLength).ThrowIfFailure(); + + // Find the first subdirectory in the path that doesn't exist + int i; + for (i = path.Length - 1; i > mountNameLength + 2; i--) + { + if (path[i] == '/') + { + string subPath = path.Substring(0, i); + + if (fs.DirectoryExists(subPath)) + { + break; + } + } + } + + // path[i] will be a '/', so skip that character + i++; + + // loop until `path.Length - 1` so CreateDirectory won't be called multiple + // times on path if the last character in the path is a '/' + for (; i < path.Length - 1; i++) + { + if (path[i] == '/') + { + string subPath = path.Substring(0, i); + + fs.CreateDirectory(subPath); + } + } + + fs.CreateDirectory(path); + } + + public static void CreateOrOverwriteFile(this FileSystemClient fs, string path, long size) + { + fs.CreateOrOverwriteFile(path, size, CreateFileOptions.None); + } + + public static void CreateOrOverwriteFile(this FileSystemClient fs, string path, long size, CreateFileOptions options) + { + path = PathTools.Normalize(path); + + if (fs.FileExists(path)) fs.DeleteFile(path); + + fs.CreateFile(path, size, CreateFileOptions.None); + } + + internal static bool IsEnabledFileSystemAccessorAccessLog(this FileSystemClient fs, string mountName) + { + if (fs.MountTable.Find(mountName, out FileSystemAccessor accessor).IsFailure()) + { + return true; + } + + return accessor.IsAccessLogEnabled; + } + } +} diff --git a/src/LibHac/Fs/FileSystemManager.cs b/src/LibHac/Fs/FileSystemManager.cs deleted file mode 100644 index 973ce264..00000000 --- a/src/LibHac/Fs/FileSystemManager.cs +++ /dev/null @@ -1,578 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using LibHac.Fs.Accessors; - -namespace LibHac.Fs -{ - public class FileSystemManager - { - internal Horizon Os { get; } - internal ITimeSpanGenerator Time { get; } - private IAccessLog AccessLog { get; set; } - - internal MountTable MountTable { get; } = new MountTable(); - - private bool AccessLogEnabled { get; set; } - - public FileSystemManager(Horizon os) - { - Os = os; - } - - public FileSystemManager(Horizon os, ITimeSpanGenerator timer) - { - Os = os; - Time = timer; - } - - public void Register(string mountName, IFileSystem fileSystem) - { - var accessor = new FileSystemAccessor(mountName, fileSystem, this); - - MountTable.Mount(accessor).ThrowIfFailure(); - - accessor.IsAccessLogEnabled = IsEnabledAccessLog(); - } - - public void Unmount(string mountName) - { - MountTable.Find(mountName, out FileSystemAccessor fileSystem).ThrowIfFailure(); - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - MountTable.Unmount(mountName); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", name: \"{mountName}\""); - } - else - { - MountTable.Unmount(mountName); - } - } - - public void SetAccessLog(bool isEnabled, IAccessLog accessLog = null) - { - AccessLogEnabled = isEnabled; - - if (accessLog != null) AccessLog = accessLog; - } - - public void CreateDirectory(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - fileSystem.CreateDirectory(subPath.ToString()); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{path}\""); - } - else - { - fileSystem.CreateDirectory(subPath.ToString()); - } - } - - public void CreateFile(string path, long size) - { - CreateFile(path, size, CreateFileOptions.None); - } - - public void CreateFile(string path, long size, CreateFileOptions options) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - fileSystem.CreateFile(subPath.ToString(), size, options); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{path}\", size: {size}"); - } - else - { - fileSystem.CreateFile(subPath.ToString(), size, options); - } - } - - public void DeleteDirectory(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - fileSystem.DeleteDirectory(subPath.ToString()); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{path}\""); - } - else - { - fileSystem.DeleteDirectory(subPath.ToString()); - } - } - - public void DeleteDirectoryRecursively(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - fileSystem.DeleteDirectoryRecursively(subPath.ToString()); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{path}\""); - } - else - { - fileSystem.DeleteDirectoryRecursively(subPath.ToString()); - } - } - - public void CleanDirectoryRecursively(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - fileSystem.CleanDirectoryRecursively(subPath.ToString()); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{path}\""); - } - else - { - fileSystem.CleanDirectoryRecursively(subPath.ToString()); - } - } - - public void DeleteFile(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - fileSystem.DeleteFile(subPath.ToString()); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{path}\""); - } - else - { - fileSystem.DeleteFile(subPath.ToString()); - } - } - - public void RenameDirectory(string oldPath, string newPath) - { - FindFileSystem(oldPath.AsSpan(), out FileSystemAccessor oldFileSystem, out ReadOnlySpan oldSubPath) - .ThrowIfFailure(); - - FindFileSystem(newPath.AsSpan(), out FileSystemAccessor newFileSystem, out ReadOnlySpan newSubPath) - .ThrowIfFailure(); - - if (oldFileSystem != newFileSystem) - { - ThrowHelper.ThrowResult(ResultFs.DifferentDestFileSystem); - } - - if (IsEnabledAccessLog() && oldFileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - oldFileSystem.RenameDirectory(oldSubPath.ToString(), newSubPath.ToString()); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{oldPath}\", new_path: \"{newPath}\""); - } - else - { - oldFileSystem.RenameDirectory(oldSubPath.ToString(), newSubPath.ToString()); - } - } - - public void RenameFile(string oldPath, string newPath) - { - FindFileSystem(oldPath.AsSpan(), out FileSystemAccessor oldFileSystem, out ReadOnlySpan oldSubPath) - .ThrowIfFailure(); - - FindFileSystem(newPath.AsSpan(), out FileSystemAccessor newFileSystem, out ReadOnlySpan newSubPath) - .ThrowIfFailure(); - - if (oldFileSystem != newFileSystem) - { - ThrowHelper.ThrowResult(ResultFs.DifferentDestFileSystem); - } - - if (IsEnabledAccessLog() && oldFileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - oldFileSystem.RenameFile(oldSubPath.ToString(), newSubPath.ToString()); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{oldPath}\", new_path: \"{newPath}\""); - } - else - { - oldFileSystem.RenameFile(oldSubPath.ToString(), newSubPath.ToString()); - } - } - - public DirectoryEntryType GetEntryType(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - DirectoryEntryType type; - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - type = fileSystem.GetEntryType(subPath.ToString()); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", path: \"{path}\""); - } - else - { - type = fileSystem.GetEntryType(subPath.ToString()); - } - - return type; - } - - public FileHandle OpenFile(string path, OpenMode mode) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - FileHandle handle; - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - FileAccessor file = fileSystem.OpenFile(subPath.ToString(), mode); - handle = new FileHandle(file); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, handle, $", path: \"{path}\", open_mode: {mode}"); - } - else - { - FileAccessor file = fileSystem.OpenFile(subPath.ToString(), mode); - handle = new FileHandle(file); - } - - return handle; - } - - public DirectoryHandle OpenDirectory(string path, OpenDirectoryMode mode) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - DirectoryHandle handle; - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - DirectoryAccessor dir = fileSystem.OpenDirectory(subPath.ToString(), mode); - handle = new DirectoryHandle(dir); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, handle, $", path: \"{path}\", open_mode: {mode}"); - } - else - { - DirectoryAccessor dir = fileSystem.OpenDirectory(subPath.ToString(), mode); - handle = new DirectoryHandle(dir); - } - - return handle; - } - - public long GetFreeSpaceSize(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - return fileSystem.GetFreeSpaceSize(subPath.ToString()); - } - - public long GetTotalSpaceSize(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - return fileSystem.GetTotalSpaceSize(subPath.ToString()); - } - - public FileTimeStampRaw GetFileTimeStamp(string path) - { - FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - .ThrowIfFailure(); - - return fileSystem.GetFileTimeStampRaw(subPath.ToString()); - } - - public void Commit(string mountName) - { - MountTable.Find(mountName, out FileSystemAccessor fileSystem).ThrowIfFailure(); - - if (IsEnabledAccessLog() && fileSystem.IsAccessLogEnabled) - { - TimeSpan startTime = Time.GetCurrent(); - fileSystem.Commit(); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, $", name: \"{mountName}\""); - } - else - { - fileSystem.Commit(); - } - } - - // ========================== - // Operations on file handles - // ========================== - public int ReadFile(FileHandle handle, Span destination, long offset) - { - return ReadFile(handle, destination, offset, ReadOption.None); - } - - public int ReadFile(FileHandle handle, Span destination, long offset, ReadOption option) - { - int bytesRead; - - if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) - { - TimeSpan startTime = Time.GetCurrent(); - bytesRead = handle.File.Read(destination, offset, option); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, handle, $", offset: {offset}, size: {destination.Length}"); - } - else - { - bytesRead = handle.File.Read(destination, offset, option); - } - - return bytesRead; - } - - public void WriteFile(FileHandle handle, ReadOnlySpan source, long offset) - { - WriteFile(handle, source, offset, WriteOption.None); - } - - public void WriteFile(FileHandle handle, ReadOnlySpan source, long offset, WriteOption option) - { - if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) - { - TimeSpan startTime = Time.GetCurrent(); - handle.File.Write(source, offset, option); - TimeSpan endTime = Time.GetCurrent(); - - string optionString = (option & WriteOption.Flush) == 0 ? "" : $", write_option: {option}"; - - OutputAccessLog(startTime, endTime, handle, $", offset: {offset}, size: {source.Length}{optionString}"); - } - else - { - handle.File.Write(source, offset, option); - } - } - - public void FlushFile(FileHandle handle) - { - if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) - { - TimeSpan startTime = Time.GetCurrent(); - handle.File.Flush(); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, handle, string.Empty); - } - else - { - handle.File.Flush(); - } - } - - public long GetFileSize(FileHandle handle) - { - return handle.File.GetSize(); - } - - public void SetFileSize(FileHandle handle, long size) - { - if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) - { - TimeSpan startTime = Time.GetCurrent(); - handle.File.SetSize(size); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, handle, $", size: {size}"); - } - else - { - handle.File.SetSize(size); - } - } - - public OpenMode GetFileOpenMode(FileHandle handle) - { - return handle.File.OpenMode; - } - - public void CloseFile(FileHandle handle) - { - if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) - { - TimeSpan startTime = Time.GetCurrent(); - handle.File.Dispose(); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, handle, string.Empty); - } - else - { - handle.File.Dispose(); - } - } - - // ========================== - // Operations on directory handles - // ========================== - public int GetDirectoryEntryCount(DirectoryHandle handle) - { - return handle.Directory.GetEntryCount(); - } - - public IEnumerable ReadDirectory(DirectoryHandle handle) - { - if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) - { - TimeSpan startTime = Time.GetCurrent(); - IEnumerable entries = handle.Directory.Read(); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, handle, string.Empty); - return entries; - } - - return handle.Directory.Read(); - } - - public void CloseDirectory(DirectoryHandle handle) - { - if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle)) - { - TimeSpan startTime = Time.GetCurrent(); - handle.Directory.Dispose(); - TimeSpan endTime = Time.GetCurrent(); - - OutputAccessLog(startTime, endTime, handle, string.Empty); - } - else - { - handle.Directory.Dispose(); - } - } - - internal Result FindFileSystem(ReadOnlySpan path, out FileSystemAccessor fileSystem, out ReadOnlySpan subPath) - { - fileSystem = default; - - Result result = GetMountName(path, out ReadOnlySpan mountName, out subPath); - if (result.IsFailure()) return result; - - result = MountTable.Find(mountName.ToString(), out fileSystem); - if (result.IsFailure()) return result; - - return Result.Success; - } - - internal static Result GetMountName(ReadOnlySpan path, out ReadOnlySpan mountName, out ReadOnlySpan subPath) - { - int mountLen = 0; - int maxMountLen = Math.Min(path.Length, PathTools.MountNameLength); - - for (int i = 0; i < maxMountLen; i++) - { - if (path[i] == PathTools.MountSeparator) - { - mountLen = i; - break; - } - } - - if (mountLen == 0) - { - mountName = default; - subPath = default; - - return ResultFs.InvalidMountName; - } - - mountName = path.Slice(0, mountLen); - - if (mountLen + 2 < path.Length) - { - subPath = path.Slice(mountLen + 2); - } - else - { - subPath = default; - } - - return Result.Success; - } - - internal bool IsEnabledAccessLog() - { - return AccessLogEnabled && AccessLog != null && Time != null; - } - - internal bool IsEnabledHandleAccessLog(FileHandle handle) - { - return handle.File.Parent.IsAccessLogEnabled; - } - - internal bool IsEnabledHandleAccessLog(DirectoryHandle handle) - { - return handle.Directory.Parent.IsAccessLogEnabled; - } - - internal void OutputAccessLog(TimeSpan startTime, TimeSpan endTime, string message, [CallerMemberName] string caller = "") - { - AccessLog.Log(startTime, endTime, 0, message, caller); - } - - internal void OutputAccessLog(TimeSpan startTime, TimeSpan endTime, FileHandle handle, string message, [CallerMemberName] string caller = "") - { - AccessLog.Log(startTime, endTime, handle.GetId(), message, caller); - } - - internal void OutputAccessLog(TimeSpan startTime, TimeSpan endTime, DirectoryHandle handle, string message, [CallerMemberName] string caller = "") - { - AccessLog.Log(startTime, endTime, handle.GetId(), message, caller); - } - } -} diff --git a/src/LibHac/Fs/FileSystemManagerUtils.cs b/src/LibHac/Fs/FileSystemManagerUtils.cs deleted file mode 100644 index 7e0498f7..00000000 --- a/src/LibHac/Fs/FileSystemManagerUtils.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using LibHac.Fs.Accessors; - -namespace LibHac.Fs -{ - public static class FileSystemManagerUtils - { - public static void CopyDirectory(this FileSystemManager fs, string sourcePath, string destPath, - CreateFileOptions options = CreateFileOptions.None, IProgressReport logger = null) - { - using (DirectoryHandle sourceHandle = fs.OpenDirectory(sourcePath, OpenDirectoryMode.All)) - { - foreach (DirectoryEntry entry in fs.ReadDirectory(sourceHandle)) - { - string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); - string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); - - if (entry.Type == DirectoryEntryType.Directory) - { - fs.EnsureDirectoryExists(subDstPath); - - fs.CopyDirectory(subSrcPath, subDstPath, options, logger); - } - - if (entry.Type == DirectoryEntryType.File) - { - logger?.LogMessage(subSrcPath); - fs.CreateOrOverwriteFile(subDstPath, entry.Size, options); - - fs.CopyFile(subSrcPath, subDstPath, logger); - } - } - } - } - - public static void CopyFile(this FileSystemManager fs, string sourcePath, string destPath, IProgressReport logger = null) - { - using (FileHandle sourceHandle = fs.OpenFile(sourcePath, OpenMode.Read)) - using (FileHandle destHandle = fs.OpenFile(destPath, OpenMode.Write | OpenMode.Append)) - { - const int maxBufferSize = 0x10000; - - long fileSize = fs.GetFileSize(sourceHandle); - int bufferSize = (int)Math.Min(maxBufferSize, fileSize); - - logger?.SetTotal(fileSize); - - byte[] buffer = ArrayPool.Shared.Rent(bufferSize); - try - { - for (long offset = 0; offset < fileSize; offset += bufferSize) - { - int toRead = (int)Math.Min(fileSize - offset, bufferSize); - Span buf = buffer.AsSpan(0, toRead); - - fs.ReadFile(sourceHandle, buf, offset); - fs.WriteFile(destHandle, buf, offset); - - logger?.ReportAdd(toRead); - } - } - finally - { - ArrayPool.Shared.Return(buffer); - logger?.SetTotal(0); - } - - fs.FlushFile(destHandle); - } - } - - public static IEnumerable EnumerateEntries(this FileSystemManager fs, string path) - { - return fs.EnumerateEntries(path, "*"); - } - - public static IEnumerable EnumerateEntries(this FileSystemManager fs, string path, string searchPattern) - { - return fs.EnumerateEntries(path, searchPattern, SearchOptions.RecurseSubdirectories); - } - - public static IEnumerable EnumerateEntries(this FileSystemManager fs, string path, string searchPattern, SearchOptions searchOptions) - { - bool ignoreCase = searchOptions.HasFlag(SearchOptions.CaseInsensitive); - bool recurse = searchOptions.HasFlag(SearchOptions.RecurseSubdirectories); - - using (DirectoryHandle sourceHandle = fs.OpenDirectory(path, OpenDirectoryMode.All)) - { - foreach (DirectoryEntry entry in fs.ReadDirectory(sourceHandle)) - { - if (PathTools.MatchesPattern(searchPattern, entry.Name, ignoreCase)) - { - yield return entry; - } - - if (entry.Type != DirectoryEntryType.Directory || !recurse) continue; - - string subPath = PathTools.Normalize(PathTools.Combine(path, entry.Name)); - - IEnumerable subEntries = fs.EnumerateEntries(subPath, searchPattern, searchOptions); - - foreach (DirectoryEntry subEntry in subEntries) - { - subEntry.FullPath = PathTools.Combine(path, subEntry.Name); - yield return subEntry; - } - } - } - } - - public static bool DirectoryExists(this FileSystemManager fs, string path) - { - return fs.GetEntryType(path) == DirectoryEntryType.Directory; - } - - public static bool FileExists(this FileSystemManager fs, string path) - { - return fs.GetEntryType(path) == DirectoryEntryType.File; - } - - public static void EnsureDirectoryExists(this FileSystemManager fs, string path) - { - path = PathTools.Normalize(path); - if (fs.DirectoryExists(path)) return; - - PathTools.GetMountNameLength(path, out int mountNameLength).ThrowIfFailure(); - - // Find the first subdirectory in the path that doesn't exist - int i; - for (i = path.Length - 1; i > mountNameLength + 2; i--) - { - if (path[i] == '/') - { - string subPath = path.Substring(0, i); - - if (fs.DirectoryExists(subPath)) - { - break; - } - } - } - - // path[i] will be a '/', so skip that character - i++; - - // loop until `path.Length - 1` so CreateDirectory won't be called multiple - // times on path if the last character in the path is a '/' - for (; i < path.Length - 1; i++) - { - if (path[i] == '/') - { - string subPath = path.Substring(0, i); - - fs.CreateDirectory(subPath); - } - } - - fs.CreateDirectory(path); - } - - public static void CreateOrOverwriteFile(this FileSystemManager fs, string path, long size) - { - fs.CreateOrOverwriteFile(path, size, CreateFileOptions.None); - } - - public static void CreateOrOverwriteFile(this FileSystemManager fs, string path, long size, CreateFileOptions options) - { - path = PathTools.Normalize(path); - - if (fs.FileExists(path)) fs.DeleteFile(path); - - fs.CreateFile(path, size, CreateFileOptions.None); - } - } -} diff --git a/src/LibHac/Fs/FsEnums.cs b/src/LibHac/Fs/FsEnums.cs new file mode 100644 index 00000000..ba9485b6 --- /dev/null +++ b/src/LibHac/Fs/FsEnums.cs @@ -0,0 +1,130 @@ +using System; + +namespace LibHac.Fs +{ + public enum BisPartitionId + { + BootPartition1Root = 0, + BootPartition2Root = 10, + UserDataRoot = 20, + BootConfigAndPackage2Part1 = 21, + BootConfigAndPackage2Part2 = 22, + BootConfigAndPackage2Part3 = 23, + BootConfigAndPackage2Part4 = 24, + BootConfigAndPackage2Part5 = 25, + BootConfigAndPackage2Part6 = 26, + CalibrationBinary = 27, + CalibrationFile = 28, + SafeMode = 29, + User = 30, + System = 31, + SystemProperEncryption = 32, + SystemProperPartition = 33, + Invalid = 35 + } + + public enum ContentStorageId + { + System = 0, + User = 1, + SdCard = 2 + } + + public enum GameCardPartition + { + Update = 0, + Normal = 1, + Secure = 2, + Logo = 3 + } + + public enum GameCardPartitionRaw + { + Normal = 0, + Secure = 1, + Writable = 2 + } + + public enum SaveDataSpaceId : byte + { + System = 0, + User = 1, + SdSystem = 2, + TemporaryStorage = 3, + SdCache = 4, + ProperSystem = 100, + Safe = 101, + BisAuto = 127 + } + + public enum CustomStorageId + { + User = 0, + SdCard = 1 + } + + public enum FileSystemType + { + Code = 0, + Data = 1, + Logo = 2, + ContentControl = 3, + ContentManual = 4, + ContentMeta = 5, + ContentData = 6, + ApplicationPackage = 7, + RegisteredUpdate = 8 + } + + public enum SaveMetaType : byte + { + None = 0, + Thumbnail = 1, + ExtensionInfo = 2 + } + + public enum ImageDirectoryId + { + Nand = 0, + SdCard = 1 + } + + public enum CloudBackupWorkStorageId + { + Nand = 0, + SdCard = 1 + } + + /// + /// Specifies which operations are available on an . + /// + [Flags] + public enum OpenMode + { + Read = 1, + Write = 2, + AllowAppend = 4, + ReadWrite = Read | Write + } + + [Flags] + public enum ReadOption + { + None = 0 + } + + [Flags] + public enum WriteOption + { + None = 0, + Flush = 1 + } + + public enum OperationId + { + Clear = 0, + ClearSignature = 1, + InvalidateCache = 2, + QueryRange = 3 + } +} diff --git a/src/LibHac/Fs/GameCard.cs b/src/LibHac/Fs/GameCard.cs new file mode 100644 index 00000000..7b2058b7 --- /dev/null +++ b/src/LibHac/Fs/GameCard.cs @@ -0,0 +1,130 @@ +using System; +using LibHac.Common; +using LibHac.FsService; + +namespace LibHac.Fs +{ + public static class GameCard + { + public static Result OpenGameCardPartition(this FileSystemClient fs, out IStorage storage, + GameCardHandle handle, GameCardPartitionRaw partitionType) + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + return fsProxy.OpenGameCardStorage(out storage, handle, partitionType); + } + + public static Result MountGameCardPartition(this FileSystemClient fs, U8Span mountName, GameCardHandle handle, + GameCardPartition partitionId) + { + Result rc = MountHelpers.CheckMountNameAcceptingReservedMountName(mountName); + if (rc.IsFailure()) return rc; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + rc = fsProxy.OpenGameCardFileSystem(out IFileSystem cardFs, handle, partitionId); + if (rc.IsFailure()) return rc; + + var mountNameGenerator = new GameCardCommonMountNameGenerator(handle, partitionId); + + return fs.Register(mountName, cardFs, mountNameGenerator); + } + + public static Result GetGameCardHandle(this FileSystemClient fs, out GameCardHandle handle) + { + handle = default; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + Result rc = fsProxy.OpenDeviceOperator(out IDeviceOperator deviceOperator); + if (rc.IsFailure()) return rc; + + return deviceOperator.GetGameCardHandle(out handle); + } + + public static bool IsGameCardInserted(this FileSystemClient fs) + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + Result rc = fsProxy.OpenDeviceOperator(out IDeviceOperator deviceOperator); + if (rc.IsFailure()) throw new LibHacException("Abort"); + + rc = deviceOperator.IsGameCardInserted(out bool isInserted); + if (rc.IsFailure()) throw new LibHacException("Abort"); + + return isInserted; + } + + public static long GetGameCardSizeBytes(GameCardSize size) + { + switch (size) + { + case GameCardSize.Size1Gb: return 0x3B800000; + case GameCardSize.Size2Gb: return 0x77000000; + case GameCardSize.Size4Gb: return 0xEE000000; + case GameCardSize.Size8Gb: return 0x1DC000000; + case GameCardSize.Size16Gb: return 0x3B8000000; + case GameCardSize.Size32Gb: return 0x770000000; + default: + throw new ArgumentOutOfRangeException(nameof(size), size, null); + } + } + + public static long CardPageToOffset(int page) + { + return (long)page << 9; + } + + private class GameCardCommonMountNameGenerator : ICommonMountNameGenerator + { + private GameCardHandle Handle { get; } + private GameCardPartition PartitionId { get; } + + public GameCardCommonMountNameGenerator(GameCardHandle handle, GameCardPartition partitionId) + { + Handle = handle; + PartitionId = partitionId; + } + + public Result Generate(Span nameBuffer) + { + char letter = GetPartitionMountLetter(PartitionId); + + string mountName = $"@Gc{letter}{Handle.Value:x8}"; + new U8Span(mountName).Value.CopyTo(nameBuffer); + + return Result.Success; + } + + private static char GetPartitionMountLetter(GameCardPartition partition) + { + switch (partition) + { + case GameCardPartition.Update: return 'U'; + case GameCardPartition.Normal: return 'N'; + case GameCardPartition.Secure: return 'S'; + default: + throw new ArgumentOutOfRangeException(nameof(partition), partition, null); + } + } + } + } + + public enum GameCardSize + { + Size1Gb = 0xFA, + Size2Gb = 0xF8, + Size4Gb = 0xF0, + Size8Gb = 0xE0, + Size16Gb = 0xE1, + Size32Gb = 0xE2 + } + + [Flags] + public enum GameCardAttribute : byte + { + AutoBoot = 1 << 0, + HistoryErase = 1 << 1, + RepairTool = 1 << 2 + } +} diff --git a/src/LibHac/Fs/IAccessLog.cs b/src/LibHac/Fs/IAccessLog.cs new file mode 100644 index 00000000..6ad9f3ca --- /dev/null +++ b/src/LibHac/Fs/IAccessLog.cs @@ -0,0 +1,10 @@ +using System; +using System.Runtime.CompilerServices; + +namespace LibHac.Fs +{ + public interface IAccessLog + { + void Log(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = ""); + } +} \ No newline at end of file diff --git a/src/LibHac/Fs/IAttributeFileSystem.cs b/src/LibHac/Fs/IAttributeFileSystem.cs index e5a4858d..1936e8db 100644 --- a/src/LibHac/Fs/IAttributeFileSystem.cs +++ b/src/LibHac/Fs/IAttributeFileSystem.cs @@ -2,8 +2,9 @@ { public interface IAttributeFileSystem : IFileSystem { - NxFileAttributes GetFileAttributes(string path); - void SetFileAttributes(string path, NxFileAttributes attributes); - long GetFileSize(string path); + Result CreateDirectory(string path, NxFileAttributes archiveAttribute); + Result GetFileAttributes(string path, out NxFileAttributes attributes); + Result SetFileAttributes(string path, NxFileAttributes attributes); + Result GetFileSize(out long fileSize, string path); } } diff --git a/src/LibHac/Fs/ICommonMountNameGenerator.cs b/src/LibHac/Fs/ICommonMountNameGenerator.cs new file mode 100644 index 00000000..53838931 --- /dev/null +++ b/src/LibHac/Fs/ICommonMountNameGenerator.cs @@ -0,0 +1,9 @@ +using System; + +namespace LibHac.Fs +{ + public interface ICommonMountNameGenerator + { + Result Generate(Span nameBuffer); + } +} \ No newline at end of file diff --git a/src/LibHac/Fs/IDirectory.cs b/src/LibHac/Fs/IDirectory.cs index fb214d1a..401d26d7 100644 --- a/src/LibHac/Fs/IDirectory.cs +++ b/src/LibHac/Fs/IDirectory.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System; namespace LibHac.Fs { @@ -8,32 +8,24 @@ namespace LibHac.Fs public interface IDirectory { /// - /// The that contains the current . + /// Retrieves the next entries that this directory contains. Does not search subdirectories. /// - IFileSystem ParentFileSystem { get; } + /// The number of s that + /// were read into . + /// The buffer the entries will be read into. + /// The of the requested operation. + /// With each call of , the object will + /// continue to iterate through all the entries it contains. + /// Each call will attempt to read as many entries as the buffer can contain. + /// Once all the entries have been read, all subsequent calls to will + /// read 0 entries into the buffer. + Result Read(out long entriesRead, Span entryBuffer); /// - /// The full path of the current in its . + /// Retrieves the number of file system entries that this directory contains. Does not search subdirectories. /// - string FullPath { get; } - - /// - /// Specifies which types of entries will be enumerated when is called. - /// - OpenDirectoryMode Mode { get; } - - /// - /// Returns an enumerable collection the file system entries of the types specified by - /// that this directory contains. Does not search subdirectories. - /// - /// An enumerable collection of file system entries in this directory. - IEnumerable Read(); - - /// - /// Returns the number of file system entries of the types specified by - /// that this directory contains. Does not search subdirectories. - /// - /// The number of child entries the directory contains. - int GetEntryCount(); + /// The number of child entries the directory contains. + /// The of the requested operation. + Result GetEntryCount(out long entryCount); } } \ No newline at end of file diff --git a/src/LibHac/Fs/IFile.cs b/src/LibHac/Fs/IFile.cs index 22bef0bf..29746104 100644 --- a/src/LibHac/Fs/IFile.cs +++ b/src/LibHac/Fs/IFile.cs @@ -14,55 +14,60 @@ namespace LibHac.Fs /// or write as many bytes as it can and return that number of bytes to the caller. /// /// - If is called on an offset past the end of the , - /// the mode is set and the file supports expansion, + /// the mode is set and the file supports expansion, /// the file will be expanded so that it is large enough to contain the written data. public interface IFile : IDisposable { - /// - /// The permissions mode for the current file. - /// - OpenMode Mode { get; } - /// /// Reads a sequence of bytes from the current . /// + /// If the operation returns successfully, The total number of bytes read into + /// the buffer. This can be less than the size of the buffer if the IFile is too short to fulfill the request. + /// The offset in the at which to begin reading. /// The buffer where the read bytes will be stored. /// The number of bytes read will be no larger than the length of the buffer. - /// The offset in the at which to begin reading. /// Options for reading from the . - /// The total number of bytes read into the buffer. This can be less than the - /// size of the buffer if the IFile is too short to fulfill the request. - /// is invalid. - /// The file's does not allow reading. - int Read(Span destination, long offset, ReadOption options); + /// The of the requested operation. + Result Read(out long bytesRead, long offset, Span destination, ReadOption options); /// /// Writes a sequence of bytes to the current . /// - /// The buffer containing the bytes to be written. /// The offset in the at which to begin writing. + /// The buffer containing the bytes to be written. /// Options for writing to the . - /// is negative. - /// The file's does not allow this request. - void Write(ReadOnlySpan source, long offset, WriteOption options); + /// The of the requested operation. + Result Write(long offset, ReadOnlySpan source, WriteOption options); /// /// Causes any buffered data to be written to the underlying device. /// - void Flush(); - - /// - /// Gets the number of bytes in the file. - /// - /// The length of the file in bytes. - long GetSize(); + Result Flush(); /// /// Sets the size of the file in bytes. /// /// The desired size of the file in bytes. - /// If increasing the file size, The file's - /// does not allow this appending. - void SetSize(long size); + /// The of the requested operation. + Result SetSize(long size); + + /// + /// Gets the number of bytes in the file. + /// + /// If the operation returns successfully, the length of the file in bytes. + /// The of the requested operation. + Result GetSize(out long size); + + /// + /// Performs various operations on the file. Used to extend the functionality of the interface. + /// + /// A buffer that will contain the response from the operation. + /// The operation to be performed. + /// The offset of the range to operate on. + /// The size of the range to operate on. + /// An input buffer. Size may vary depending on the operation performed. + /// The of the requested operation. + Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer); } } \ No newline at end of file diff --git a/src/LibHac/Fs/IFileSystem.cs b/src/LibHac/Fs/IFileSystem.cs index 827d2f15..91721465 100644 --- a/src/LibHac/Fs/IFileSystem.cs +++ b/src/LibHac/Fs/IFileSystem.cs @@ -1,4 +1,5 @@ using System; +using LibHac.FsSystem; namespace LibHac.Fs { @@ -7,19 +8,6 @@ namespace LibHac.Fs /// public interface IFileSystem { - /// - /// Creates all directories and subdirectories in the specified path unless they already exist. - /// - /// The full path of the directory to create. - /// - /// A will be thrown with the given under the following conditions: - /// - /// The parent directory of the specified path does not exist: - /// Specified path already exists as either a file or directory: - /// Insufficient free space to create the directory: - /// - void CreateDirectory(string path); - /// /// Creates or overwrites a file at the specified path. /// @@ -27,162 +15,191 @@ namespace LibHac.Fs /// The initial size of the created file. /// Flags to control how the file is created. /// Should usually be + /// The of the requested operation. /// - /// A will be thrown with the given under the following conditions: + /// The following codes may be returned under certain conditions: /// /// The parent directory of the specified path does not exist: /// Specified path already exists as either a file or directory: /// Insufficient free space to create the file: /// - void CreateFile(string path, long size, CreateFileOptions options); - - /// - /// Deletes the specified directory. - /// - /// The full path of the directory to delete. - /// - /// A will be thrown with the given under the following conditions: - /// - /// The specified path does not exist or is a file: - /// The specified directory is not empty: - /// - void DeleteDirectory(string path); - - /// - /// Deletes the specified directory and any subdirectories and files in the directory. - /// - /// The full path of the directory to delete. - /// - /// A will be thrown with the given under the following conditions: - /// - /// The specified path does not exist or is a file: - /// - void DeleteDirectoryRecursively(string path); - - /// - /// Deletes any subdirectories and files in the specified directory. - /// - /// The full path of the directory to clean. - /// - /// A will be thrown with the given under the following conditions: - /// - /// The specified path does not exist or is a file: - /// - void CleanDirectoryRecursively(string path); + Result CreateFile(string path, long size, CreateFileOptions options); /// /// Deletes the specified file. /// /// The full path of the file to delete. + /// The of the requested operation. /// - /// A will be thrown with the given under the following conditions: + /// The following codes may be returned under certain conditions: /// /// The specified path does not exist or is a directory: /// - void DeleteFile(string path); + Result DeleteFile(string path); /// - /// Creates an instance for enumerating the specified directory. + /// Creates all directories and subdirectories in the specified path unless they already exist. /// - /// The directory's full path. - /// Specifies which sub-entries should be enumerated. - /// An instance for the specified directory. + /// The full path of the directory to create. + /// The of the requested operation. /// - /// A will be thrown with the given under the following conditions: + /// The following codes may be returned under certain conditions: + /// + /// The parent directory of the specified path does not exist: + /// Specified path already exists as either a file or directory: + /// Insufficient free space to create the directory: + /// + Result CreateDirectory(string path); + + /// + /// Deletes the specified directory. + /// + /// The full path of the directory to delete. + /// The of the requested operation. + /// + /// The following codes may be returned under certain conditions: + /// + /// The specified path does not exist or is a file: + /// The specified directory is not empty: + /// + Result DeleteDirectory(string path); + + /// + /// Deletes the specified directory and any subdirectories and files in the directory. + /// + /// The full path of the directory to delete. + /// The of the requested operation. + /// + /// The following codes may be returned under certain conditions: /// /// The specified path does not exist or is a file: /// - IDirectory OpenDirectory(string path, OpenDirectoryMode mode); + Result DeleteDirectoryRecursively(string path); /// - /// Opens an instance for the specified path. + /// Deletes any subdirectories and files in the specified directory. /// - /// The full path of the file to open. - /// Specifies the access permissions of the created . - /// An instance for the specified path. + /// The full path of the directory to clean. + /// The of the requested operation. /// - /// A will be thrown with the given under the following conditions: + /// The following codes may be returned under certain conditions: /// - /// The specified path does not exist or is a directory: + /// The specified path does not exist or is a file: /// - IFile OpenFile(string path, OpenMode mode); - - /// - /// Renames or moves a directory to a new location. - /// - /// The full path of the directory to rename. - /// The new full path of the directory. - /// An instance for the specified path. - /// - /// If and are the same, this function does nothing and returns successfully. - /// A will be thrown with the given under the following conditions: - /// - /// does not exist or is a file: - /// 's parent directory does not exist: - /// already exists as either a file or directory: - /// Either or is a subpath of the other: - /// - void RenameDirectory(string srcPath, string dstPath); + Result CleanDirectoryRecursively(string path); /// /// Renames or moves a file to a new location. /// - /// The full path of the file to rename. - /// The new full path of the file. + /// The full path of the file to rename. + /// The new full path of the file. + /// The of the requested operation. /// - /// If and are the same, this function does nothing and returns successfully. - /// A will be thrown with the given under the following conditions: + /// If and are the same, this function does nothing and returns successfully. + /// The following codes may be returned under certain conditions: /// - /// does not exist or is a directory: - /// 's parent directory does not exist: - /// already exists as either a file or directory: + /// does not exist or is a directory: + /// 's parent directory does not exist: + /// already exists as either a file or directory: /// - void RenameFile(string srcPath, string dstPath); + Result RenameFile(string oldPath, string newPath); + + /// + /// Renames or moves a directory to a new location. + /// + /// The full path of the directory to rename. + /// The new full path of the directory. + /// The of the requested operation. + /// + /// If and are the same, this function does nothing and returns . + /// The following codes may be returned under certain conditions: + /// + /// does not exist or is a file: + /// 's parent directory does not exist: + /// already exists as either a file or directory: + /// Either or is a subpath of the other: + /// + Result RenameDirectory(string oldPath, string newPath); /// /// Determines whether the specified path is a file or directory, or does not exist. /// + /// If the operation returns successfully, the of the file. /// The full path to check. - /// The of the file. + /// The of the requested operation. /// /// This function operates slightly differently than it does in Horizon OS. /// Instead of returning when an entry is missing, /// the function will return . /// - DirectoryEntryType GetEntryType(string path); + Result GetEntryType(out DirectoryEntryType entryType, string path); /// /// Gets the amount of available free space on a drive, in bytes. /// + /// If the operation returns successfully, the amount of free space available on the drive, in bytes. /// The path of the drive to query. Unused in almost all cases. - /// The amount of free space available on the drive, in bytes. - long GetFreeSpaceSize(string path); + /// The of the requested operation. + Result GetFreeSpaceSize(out long freeSpace, string path); /// /// Gets the total size of storage space on a drive, in bytes. /// + /// If the operation returns successfully, the total size of the drive, in bytes. /// The path of the drive to query. Unused in almost all cases. - /// The total size of the drive, in bytes. - long GetTotalSpaceSize(string path); + /// The of the requested operation. + Result GetTotalSpaceSize(out long totalSpace, string path); /// - /// Gets the creation, last accessed, and last modified timestamps of a file or directory. + /// Opens an instance for the specified path. /// - /// The path of the file or directory. - /// The timestamps for the specified file or directory. - /// This value is expressed as a Unix timestamp + /// If the operation returns successfully, + /// An instance for the specified path. + /// The full path of the file to open. + /// Specifies the access permissions of the created . + /// The of the requested operation. /// - /// A will be thrown with the given under the following conditions: + /// The following codes may be returned under certain conditions: /// - /// The specified path does not exist: + /// The specified path does not exist or is a directory: /// - FileTimeStampRaw GetFileTimeStampRaw(string path); + Result OpenFile(out IFile file, string path, OpenMode mode); + + /// + /// Creates an instance for enumerating the specified directory. + /// + /// If the operation returns successfully, + /// An instance for the specified directory. + /// The directory's full path. + /// Specifies which sub-entries should be enumerated. + /// The of the requested operation. + /// + /// The following codes may be returned under certain conditions: + /// + /// The specified path does not exist or is a file: + /// + Result OpenDirectory(out IDirectory directory, string path, OpenDirectoryMode mode); /// /// Commits any changes to a transactional file system. /// Does nothing if called on a non-transactional file system. /// - void Commit(); + /// The of the requested operation. + Result Commit(); + + /// + /// Gets the creation, last accessed, and last modified timestamps of a file or directory. + /// + /// If the operation returns successfully, the timestamps for the specified file or directory. + /// These value are expressed as Unix timestamps. + /// The path of the file or directory. + /// The of the requested operation. + /// + /// The following codes may be returned under certain conditions: + /// + /// The specified path does not exist: + /// + Result GetFileTimeStampRaw(out FileTimeStampRaw timeStamp, string path); /// /// Performs a query on the specified file. @@ -193,9 +210,10 @@ namespace LibHac.Fs /// May be unused depending on the query type. /// The buffer for sending data to the query operation. /// May be unused depending on the query type. - /// The full path of the file to query. /// The type of query to perform. - void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId); + /// The full path of the file to query. + /// The of the requested operation. + Result QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, string path); } /// @@ -204,9 +222,10 @@ namespace LibHac.Fs [Flags] public enum OpenDirectoryMode { - Directories = 1, - Files = 2, - All = Directories | Files + Directory = 1 << 0, + File = 1 << 1, + NoFileSize = 1 << 31, + All = Directory | File } /// diff --git a/src/LibHac/Fs/IStorage.cs b/src/LibHac/Fs/IStorage.cs index 7008d50c..455388ef 100644 --- a/src/LibHac/Fs/IStorage.cs +++ b/src/LibHac/Fs/IStorage.cs @@ -14,7 +14,7 @@ namespace LibHac.Fs /// The number of bytes read will be equal to the length of the buffer. /// The offset in the at which to begin reading. /// Invalid offset or the IStorage contains fewer bytes than requested. - void Read(Span destination, long offset); + Result Read(long offset, Span destination); /// /// Writes a sequence of bytes to the current . @@ -23,24 +23,36 @@ namespace LibHac.Fs /// The offset in the at which to begin writing. /// Invalid offset or /// is too large to be written to the IStorage. - void Write(ReadOnlySpan source, long offset); + Result Write(long offset, ReadOnlySpan source); /// /// Causes any buffered data to be written to the underlying device. /// - void Flush(); + Result Flush(); /// /// Sets the size of the current IStorage. /// /// The desired size of the current IStorage in bytes. - void SetSize(long size); + Result SetSize(long size); /// /// The size of the. -1 will be returned if /// the cannot be represented as a sequence of contiguous bytes. /// /// The size of the in bytes. - long GetSize(); + Result GetSize(out long size); + + /// + /// Performs various operations on the file. Used to extend the functionality of the interface. + /// + /// A buffer that will contain the response from the operation. + /// The operation to be performed. + /// The offset of the range to operate on. + /// The size of the range to operate on. + /// An input buffer. Size may vary depending on the operation performed. + /// The of the requested operation. + Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer); } } diff --git a/src/LibHac/Fs/LayeredFileSystem.cs b/src/LibHac/Fs/LayeredFileSystem.cs deleted file mode 100644 index d0e23930..00000000 --- a/src/LibHac/Fs/LayeredFileSystem.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace LibHac.Fs -{ - public class LayeredFileSystem : IFileSystem - { - private List Sources { get; } = new List(); - - public LayeredFileSystem(IList sourceFileSystems) - { - Sources.AddRange(sourceFileSystems); - } - - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) - { - path = PathTools.Normalize(path); - - var dirs = new List(); - - foreach (IFileSystem fs in Sources) - { - DirectoryEntryType type = fs.GetEntryType(path); - - if (type == DirectoryEntryType.File && dirs.Count == 0) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - if (fs.GetEntryType(path) == DirectoryEntryType.Directory) - { - dirs.Add(fs.OpenDirectory(path, mode)); - } - } - - var dir = new LayeredFileSystemDirectory(this, dirs, path, mode); - - return dir; - } - - public IFile OpenFile(string path, OpenMode mode) - { - path = PathTools.Normalize(path); - - foreach (IFileSystem fs in Sources) - { - DirectoryEntryType type = fs.GetEntryType(path); - - if (type == DirectoryEntryType.File) - { - return fs.OpenFile(path, mode); - } - - if (type == DirectoryEntryType.Directory) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - } - - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - return default; - } - - public DirectoryEntryType GetEntryType(string path) - { - path = PathTools.Normalize(path); - - foreach (IFileSystem fs in Sources) - { - DirectoryEntryType type = fs.GetEntryType(path); - - if (type != DirectoryEntryType.NotFound) return type; - } - - return DirectoryEntryType.NotFound; - } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - path = PathTools.Normalize(path); - - foreach (IFileSystem fs in Sources) - { - if (fs.GetEntryType(path) != DirectoryEntryType.NotFound) - { - return fs.GetFileTimeStampRaw(path); - } - } - - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - return default; - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) - { - path = PathTools.Normalize(path); - - foreach (IFileSystem fs in Sources) - { - if (fs.GetEntryType(path) != DirectoryEntryType.NotFound) - { - fs.QueryEntry(outBuffer, inBuffer, path, queryId); - return; - } - } - - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - public void Commit() { } - - public void CreateDirectory(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - public void CreateFile(string path, long size, CreateFileOptions options) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - public void DeleteDirectory(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - public void DeleteDirectoryRecursively(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - public void CleanDirectoryRecursively(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - public void DeleteFile(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - public void RenameDirectory(string srcPath, string dstPath) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - public void RenameFile(string srcPath, string dstPath) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - - public long GetFreeSpaceSize(string path) - { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - return default; - } - - public long GetTotalSpaceSize(string path) - { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - return default; - } - } -} diff --git a/src/LibHac/Fs/LayeredFileSystemDirectory.cs b/src/LibHac/Fs/LayeredFileSystemDirectory.cs deleted file mode 100644 index ee980bc4..00000000 --- a/src/LibHac/Fs/LayeredFileSystemDirectory.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace LibHac.Fs -{ - public class LayeredFileSystemDirectory : IDirectory - { - public IFileSystem ParentFileSystem { get; } - - public string FullPath { get; } - public OpenDirectoryMode Mode { get; } - - private List Sources { get; } - - public LayeredFileSystemDirectory(IFileSystem fs, List sources, string path, OpenDirectoryMode mode) - { - ParentFileSystem = fs; - Sources = sources; - FullPath = path; - Mode = mode; - } - - public IEnumerable Read() - { - var returnedFiles = new HashSet(); - - foreach (IDirectory source in Sources) - { - foreach (DirectoryEntry entry in source.Read()) - { - if (returnedFiles.Contains(entry.FullPath)) continue; - - returnedFiles.Add(entry.FullPath); - yield return entry; - } - } - } - - public int GetEntryCount() - { - return Read().Count(); - } - } -} diff --git a/src/LibHac/Fs/LocalDirectory.cs b/src/LibHac/Fs/LocalDirectory.cs deleted file mode 100644 index be1069b7..00000000 --- a/src/LibHac/Fs/LocalDirectory.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.CompilerServices; - -namespace LibHac.Fs -{ - public class LocalDirectory : IDirectory - { - public IFileSystem ParentFileSystem { get; } - public string FullPath { get; } - - private string LocalPath { get; } - public OpenDirectoryMode Mode { get; } - private DirectoryInfo DirInfo { get; } - - public LocalDirectory(LocalFileSystem fs, string path, OpenDirectoryMode mode) - { - ParentFileSystem = fs; - FullPath = path; - LocalPath = fs.ResolveLocalPath(path); - Mode = mode; - - try - { - DirInfo = new DirectoryInfo(LocalPath); - } - catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || - ex is PathTooLongException) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - - if (!DirInfo.Exists) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - } - - public IEnumerable Read() - { - foreach (FileSystemInfo entry in DirInfo.EnumerateFileSystemInfos()) - { - bool isDir = (entry.Attributes & FileAttributes.Directory) != 0; - - if (!CanReturnEntry(isDir, Mode)) continue; - - DirectoryEntryType type = isDir ? DirectoryEntryType.Directory : DirectoryEntryType.File; - long length = isDir ? 0 : ((FileInfo)entry).Length; - - yield return new DirectoryEntry(entry.Name, PathTools.Combine(FullPath, entry.Name), type, length) - { - Attributes = entry.Attributes.ToNxAttributes() - }; - } - } - - public int GetEntryCount() - { - int count = 0; - - foreach (FileSystemInfo entry in DirInfo.EnumerateFileSystemInfos()) - { - bool isDir = (entry.Attributes & FileAttributes.Directory) != 0; - - if (CanReturnEntry(isDir, Mode)) count++; - } - - return count; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool CanReturnEntry(bool isDir, OpenDirectoryMode mode) - { - return isDir && (mode & OpenDirectoryMode.Directories) != 0 || - !isDir && (mode & OpenDirectoryMode.Files) != 0; - } - } -} diff --git a/src/LibHac/Fs/LocalFile.cs b/src/LibHac/Fs/LocalFile.cs deleted file mode 100644 index 950af64f..00000000 --- a/src/LibHac/Fs/LocalFile.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.IO; - -namespace LibHac.Fs -{ - public class LocalFile : FileBase - { - private const int ErrorHandleDiskFull = unchecked((int)0x80070027); - private const int ErrorDiskFull = unchecked((int)0x80070070); - - private FileStream Stream { get; } - private StreamFile File { get; } - - public LocalFile(string path, OpenMode mode) - { - Mode = mode; - Stream = OpenFile(path, mode); - File = new StreamFile(Stream, mode); - - ToDispose.Add(File); - ToDispose.Add(Stream); - } - - public override int Read(Span destination, long offset, ReadOption options) - { - int toRead = ValidateReadParamsAndGetSize(destination, offset); - - File.Read(destination.Slice(0, toRead), offset, options); - - return toRead; - } - - public override void Write(ReadOnlySpan source, long offset, WriteOption options) - { - ValidateWriteParams(source, offset); - - File.Write(source, offset, options); - } - - public override void Flush() - { - File.Flush(); - } - - public override long GetSize() - { - return File.GetSize(); - } - - public override void SetSize(long size) - { - try - { - File.SetSize(size); - } - catch (IOException ex) when (ex.HResult == ErrorDiskFull || ex.HResult == ErrorHandleDiskFull) - { - ThrowHelper.ThrowResult(ResultFs.InsufficientFreeSpace, ex); - throw; - } - } - - private static FileAccess GetFileAccess(OpenMode mode) - { - // FileAccess and OpenMode have the same flags - return (FileAccess)(mode & OpenMode.ReadWrite); - } - - private static FileShare GetFileShare(OpenMode mode) - { - return mode.HasFlag(OpenMode.Write) ? FileShare.Read : FileShare.ReadWrite; - } - - private static FileStream OpenFile(string path, OpenMode mode) - { - try - { - return new FileStream(path, FileMode.Open, GetFileAccess(mode), GetFileShare(mode)); - } - catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || - ex is PathTooLongException || ex is DirectoryNotFoundException || - ex is FileNotFoundException || ex is NotSupportedException) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) - { - // todo: Should a HorizonResultException be thrown? - throw; - } - } - } -} diff --git a/src/LibHac/Fs/LocalFileSystem.cs b/src/LibHac/Fs/LocalFileSystem.cs deleted file mode 100644 index 048e5c71..00000000 --- a/src/LibHac/Fs/LocalFileSystem.cs +++ /dev/null @@ -1,480 +0,0 @@ -using System; -using System.IO; -using System.Security; - -namespace LibHac.Fs -{ - public class LocalFileSystem : IAttributeFileSystem - { - private const int ErrorHandleDiskFull = unchecked((int)0x80070027); - private const int ErrorFileExists = unchecked((int)0x80070050); - private const int ErrorDiskFull = unchecked((int)0x80070070); - private const int ErrorDirNotEmpty = unchecked((int)0x80070091); - - private string BasePath { get; } - - /// - /// Opens a directory on local storage as an . - /// The directory will be created if it does not exist. - /// - /// The path that will be the root of the . - public LocalFileSystem(string basePath) - { - BasePath = Path.GetFullPath(basePath); - - if (!Directory.Exists(BasePath)) - { - Directory.CreateDirectory(BasePath); - } - } - - internal string ResolveLocalPath(string path) - { - return PathTools.Combine(BasePath, path); - } - - public NxFileAttributes GetFileAttributes(string path) - { - path = PathTools.Normalize(path); - return File.GetAttributes(ResolveLocalPath(path)).ToNxAttributes(); - } - - public void SetFileAttributes(string path, NxFileAttributes attributes) - { - path = PathTools.Normalize(path); - string localPath = ResolveLocalPath(path); - - FileAttributes attributesOld = File.GetAttributes(localPath); - FileAttributes attributesNew = attributesOld.ApplyNxAttributes(attributes); - - File.SetAttributes(localPath, attributesNew); - } - - public long GetFileSize(string path) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - FileInfo info = GetFileInfo(localPath); - return GetSizeInternal(info); - } - - public void CreateDirectory(string path) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - DirectoryInfo dir = GetDirInfo(localPath); - - if (dir.Exists) - { - ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists); - } - - if (dir.Parent?.Exists != true) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - CreateDirInternal(dir); - } - - public void CreateFile(string path, long size, CreateFileOptions options) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - FileInfo file = GetFileInfo(localPath); - - if (file.Exists) - { - ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists); - } - - if (file.Directory?.Exists != true) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - using (FileStream stream = CreateFileInternal(file)) - { - SetStreamLengthInternal(stream, size); - } - } - - public void DeleteDirectory(string path) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - DirectoryInfo dir = GetDirInfo(localPath); - - DeleteDirectoryInternal(dir, false); - } - - public void DeleteDirectoryRecursively(string path) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - DirectoryInfo dir = GetDirInfo(localPath); - - DeleteDirectoryInternal(dir, true); - } - - public void CleanDirectoryRecursively(string path) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - foreach (string file in Directory.EnumerateFiles(localPath)) - { - DeleteFileInternal(GetFileInfo(file)); - } - - foreach (string dir in Directory.EnumerateDirectories(localPath)) - { - DeleteDirectoryInternal(GetDirInfo(dir), true); - } - } - - public void DeleteFile(string path) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - FileInfo file = GetFileInfo(localPath); - - DeleteFileInternal(file); - } - - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) - { - path = PathTools.Normalize(path); - - if (GetEntryType(path) == DirectoryEntryType.File) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - return new LocalDirectory(this, path, mode); - } - - public IFile OpenFile(string path, OpenMode mode) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - if (GetEntryType(path) == DirectoryEntryType.Directory) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); - } - - return new LocalFile(localPath, mode); - } - - public void RenameDirectory(string srcPath, string dstPath) - { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); - - // Official FS behavior is to do nothing in this case - if (srcPath == dstPath) return; - - // FS does the subpath check before verifying the path exists - if (PathTools.IsSubPath(srcPath.AsSpan(), dstPath.AsSpan())) - { - ThrowHelper.ThrowResult(ResultFs.DestinationIsSubPathOfSource); - } - - DirectoryInfo srcDir = GetDirInfo(ResolveLocalPath(srcPath)); - DirectoryInfo dstDir = GetDirInfo(ResolveLocalPath(dstPath)); - - RenameDirInternal(srcDir, dstDir); - } - - public void RenameFile(string srcPath, string dstPath) - { - string srcLocalPath = ResolveLocalPath(PathTools.Normalize(srcPath)); - string dstLocalPath = ResolveLocalPath(PathTools.Normalize(dstPath)); - - // Official FS behavior is to do nothing in this case - if (srcLocalPath == dstLocalPath) return; - - FileInfo srcFile = GetFileInfo(srcLocalPath); - FileInfo dstFile = GetFileInfo(dstLocalPath); - - RenameFileInternal(srcFile, dstFile); - } - - public DirectoryEntryType GetEntryType(string path) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - DirectoryInfo dir = GetDirInfo(localPath); - - if (dir.Exists) - { - return DirectoryEntryType.Directory; - } - - FileInfo file = GetFileInfo(localPath); - - if (file.Exists) - { - return DirectoryEntryType.File; - } - - return DirectoryEntryType.NotFound; - } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - string localPath = ResolveLocalPath(PathTools.Normalize(path)); - - if (!GetFileInfo(localPath).Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound); - - FileTimeStampRaw timeStamp = default; - - timeStamp.Created = new DateTimeOffset(File.GetCreationTime(localPath)).ToUnixTimeSeconds(); - timeStamp.Accessed = new DateTimeOffset(File.GetLastAccessTime(localPath)).ToUnixTimeSeconds(); - timeStamp.Modified = new DateTimeOffset(File.GetLastWriteTime(localPath)).ToUnixTimeSeconds(); - - return timeStamp; - } - - public long GetFreeSpaceSize(string path) - { - return new DriveInfo(BasePath).AvailableFreeSpace; - } - - public long GetTotalSpaceSize(string path) - { - return new DriveInfo(BasePath).TotalSize; - } - - public void Commit() { } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperation); - - private static long GetSizeInternal(FileInfo file) - { - try - { - return file.Length; - } - catch (FileNotFoundException ex) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - catch (Exception ex) when (ex is SecurityException || ex is UnauthorizedAccessException) - { - // todo: Should a HorizonResultException be thrown? - throw; - } - } - - private static FileStream CreateFileInternal(FileInfo file) - { - try - { - return new FileStream(file.FullName, FileMode.CreateNew, FileAccess.ReadWrite); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - catch (IOException ex) when (ex.HResult == ErrorDiskFull || ex.HResult == ErrorHandleDiskFull) - { - ThrowHelper.ThrowResult(ResultFs.InsufficientFreeSpace, ex); - throw; - } - catch (IOException ex) when (ex.HResult == ErrorFileExists) - { - ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists, ex); - throw; - } - catch (Exception ex) when (ex is SecurityException || ex is UnauthorizedAccessException) - { - // todo: Should a HorizonResultException be thrown? - throw; - } - } - - private static void SetStreamLengthInternal(Stream stream, long size) - { - try - { - stream.SetLength(size); - } - catch (IOException ex) when (ex.HResult == ErrorDiskFull || ex.HResult == ErrorHandleDiskFull) - { - ThrowHelper.ThrowResult(ResultFs.InsufficientFreeSpace, ex); - throw; - } - } - - private static void DeleteDirectoryInternal(DirectoryInfo dir, bool recursive) - { - if (!dir.Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound); - - try - { - dir.Delete(recursive); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - catch (IOException ex) when (ex.HResult == ErrorDirNotEmpty) - { - ThrowHelper.ThrowResult(ResultFs.DirectoryNotEmpty, ex); - throw; - } - catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) - { - // todo: Should a HorizonResultException be thrown? - throw; - } - - EnsureDeleted(dir); - } - - private static void DeleteFileInternal(FileInfo file) - { - if (!file.Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound); - - try - { - file.Delete(); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - catch (IOException ex) when (ex.HResult == ErrorDirNotEmpty) - { - ThrowHelper.ThrowResult(ResultFs.DirectoryNotEmpty, ex); - throw; - } - catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) - { - // todo: Should a HorizonResultException be thrown? - throw; - } - - EnsureDeleted(file); - } - - private static void CreateDirInternal(DirectoryInfo dir) - { - try - { - dir.Create(); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - catch (IOException ex) when (ex.HResult == ErrorDiskFull || ex.HResult == ErrorHandleDiskFull) - { - ThrowHelper.ThrowResult(ResultFs.InsufficientFreeSpace, ex); - throw; - } - catch (Exception ex) when (ex is SecurityException || ex is UnauthorizedAccessException) - { - // todo: Should a HorizonResultException be thrown? - throw; - } - } - - private static void RenameDirInternal(DirectoryInfo source, DirectoryInfo dest) - { - if (!source.Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound); - if (dest.Exists) ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists); - - try - { - source.MoveTo(dest.FullName); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) - { - // todo: Should a HorizonResultException be thrown? - throw; - } - } - - private static void RenameFileInternal(FileInfo source, FileInfo dest) - { - if (!source.Exists) ThrowHelper.ThrowResult(ResultFs.PathNotFound); - if (dest.Exists) ThrowHelper.ThrowResult(ResultFs.PathAlreadyExists); - - try - { - source.MoveTo(dest.FullName); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) - { - // todo: Should a HorizonResultException be thrown? - throw; - } - } - - - // GetFileInfo and GetDirInfo detect invalid paths - private static FileInfo GetFileInfo(string path) - { - try - { - return new FileInfo(path); - } - catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || - ex is PathTooLongException) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - } - - private static DirectoryInfo GetDirInfo(string path) - { - try - { - return new DirectoryInfo(path); - } - catch (Exception ex) when (ex is ArgumentNullException || ex is ArgumentException || - ex is PathTooLongException) - { - ThrowHelper.ThrowResult(ResultFs.PathNotFound, ex); - throw; - } - } - - // Delete operations on IFileSystem should be synchronous - // DeleteFile and RemoveDirectory only mark the file for deletion, so we need - // to poll the filesystem until it's actually gone - private static void EnsureDeleted(FileSystemInfo entry) - { - int tries = 0; - - do - { - entry.Refresh(); - tries++; - - if (tries > 1000) - { - throw new IOException($"Unable to delete file {entry.FullName}"); - } - } while (entry.Exists); - } - } -} diff --git a/src/LibHac/Fs/LocalStorage.cs b/src/LibHac/Fs/LocalStorage.cs deleted file mode 100644 index 6d990699..00000000 --- a/src/LibHac/Fs/LocalStorage.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.IO; - -namespace LibHac.Fs -{ - public class LocalStorage : StorageBase - { - private string Path { get; } - private FileStream Stream { get; } - private StreamStorage Storage { get; } - - public LocalStorage(string path, FileAccess access) : this(path, access, FileMode.Open) { } - - public LocalStorage(string path, FileAccess access, FileMode mode) - { - Path = path; - Stream = new FileStream(Path, mode, access); - Storage = new StreamStorage(Stream, false); - - ToDispose.Add(Storage); - ToDispose.Add(Stream); - } - - protected override void ReadImpl(Span destination, long offset) - { - Storage.Read(destination, offset); - } - - protected override void WriteImpl(ReadOnlySpan source, long offset) - { - Storage.Write(source, offset); - } - - public override void Flush() - { - Storage.Flush(); - } - - public override long GetSize() => Storage.GetSize(); - } -} diff --git a/src/LibHac/Fs/MountHelpers.cs b/src/LibHac/Fs/MountHelpers.cs new file mode 100644 index 00000000..33ecb09e --- /dev/null +++ b/src/LibHac/Fs/MountHelpers.cs @@ -0,0 +1,33 @@ +using LibHac.Common; + +namespace LibHac.Fs +{ + internal static class MountHelpers + { + public static Result CheckMountName(U8Span name) + { + if (name.IsNull()) return ResultFs.NullArgument.Log(); + + if (name.Length > 0 && name[0] == '@') return ResultFs.InvalidMountName.Log(); + if (!CheckMountNameImpl(name)) return ResultFs.InvalidMountName.Log(); + + return Result.Success; + } + + public static Result CheckMountNameAcceptingReservedMountName(U8Span name) + { + if (name.IsNull()) return ResultFs.NullArgument.Log(); + + if (!CheckMountNameImpl(name)) return ResultFs.InvalidMountName.Log(); + + return Result.Success; + } + + // ReSharper disable once UnusedParameter.Local + private static bool CheckMountNameImpl(U8Span name) + { + // Todo + return true; + } + } +} diff --git a/src/LibHac/Fs/NullFile.cs b/src/LibHac/Fs/NullFile.cs deleted file mode 100644 index 71081c69..00000000 --- a/src/LibHac/Fs/NullFile.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class NullFile : FileBase - { - public NullFile() - { - Mode = OpenMode.ReadWrite; - } - - public NullFile(long length) : this() => Length = length; - - private long Length { get; } - - public override int Read(Span destination, long offset, ReadOption options) - { - int toRead = ValidateReadParamsAndGetSize(destination, offset); - destination.Slice(0, toRead).Clear(); - return toRead; - } - - public override void Write(ReadOnlySpan source, long offset, WriteOption options) - { - } - - public override void Flush() - { - } - - public override long GetSize() => Length; - - public override void SetSize(long size) - { - throw new NotSupportedException(); - } - } -} diff --git a/src/LibHac/Fs/NullStorage.cs b/src/LibHac/Fs/NullStorage.cs deleted file mode 100644 index 83c6dbfe..00000000 --- a/src/LibHac/Fs/NullStorage.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - /// - /// An that returns all zeros when read, and does nothing on write. - /// - public class NullStorage : StorageBase - { - public NullStorage() { } - public NullStorage(long length) => _length = length; - - private long _length; - - protected override void ReadImpl(Span destination, long offset) - { - destination.Clear(); - } - - protected override void WriteImpl(ReadOnlySpan source, long offset) - { - } - - public override void Flush() - { - } - - public override long GetSize() => _length; - } -} diff --git a/src/LibHac/Fs/PartitionDirectory.cs b/src/LibHac/Fs/PartitionDirectory.cs deleted file mode 100644 index 99dc6102..00000000 --- a/src/LibHac/Fs/PartitionDirectory.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.IO; - -namespace LibHac.Fs -{ - public class PartitionDirectory : IDirectory - { - IFileSystem IDirectory.ParentFileSystem => ParentFileSystem; - public PartitionFileSystem ParentFileSystem { get; } - public string FullPath { get; } - - public OpenDirectoryMode Mode { get; } - - public PartitionDirectory(PartitionFileSystem fs, string path, OpenDirectoryMode mode) - { - path = PathTools.Normalize(path); - - if (path != "/") throw new DirectoryNotFoundException(); - - ParentFileSystem = fs; - FullPath = path; - Mode = mode; - } - - - public IEnumerable Read() - { - if (Mode.HasFlag(OpenDirectoryMode.Files)) - { - foreach (PartitionFileEntry entry in ParentFileSystem.Files) - { - yield return new DirectoryEntry(entry.Name, '/' + entry.Name, DirectoryEntryType.File, entry.Size); - } - } - } - - public int GetEntryCount() - { - int count = 0; - - if (Mode.HasFlag(OpenDirectoryMode.Files)) - { - count += ParentFileSystem.Files.Length; - } - - return count; - } - } -} \ No newline at end of file diff --git a/src/LibHac/Fs/PartitionFile.cs b/src/LibHac/Fs/PartitionFile.cs deleted file mode 100644 index 44416dd4..00000000 --- a/src/LibHac/Fs/PartitionFile.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class PartitionFile : FileBase - { - private IStorage BaseStorage { get; } - private long Offset { get; } - private long Size { get; } - - public PartitionFile(IStorage baseStorage, long offset, long size, OpenMode mode) - { - Mode = mode; - BaseStorage = baseStorage; - Offset = offset; - Size = size; - } - - public override int Read(Span destination, long offset, ReadOption options) - { - int toRead = ValidateReadParamsAndGetSize(destination, offset); - - long storageOffset = Offset + offset; - BaseStorage.Read(destination.Slice(0, toRead), storageOffset); - - return toRead; - } - - public override void Write(ReadOnlySpan source, long offset, WriteOption options) - { - ValidateWriteParams(source, offset); - - BaseStorage.Write(source, offset); - - if ((options & WriteOption.Flush) != 0) - { - BaseStorage.Flush(); - } - } - - public override void Flush() - { - if ((Mode & OpenMode.Write) != 0) - { - BaseStorage.Flush(); - } - } - - public override long GetSize() - { - return Size; - } - - public override void SetSize(long size) - { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationInPartitionFileSetSize); - } - } -} diff --git a/src/LibHac/Fs/ReadOnlyDirectory.cs b/src/LibHac/Fs/ReadOnlyDirectory.cs deleted file mode 100644 index 5d8a3c27..00000000 --- a/src/LibHac/Fs/ReadOnlyDirectory.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; - -namespace LibHac.Fs -{ - public class ReadOnlyDirectory : IDirectory - { - private IDirectory BaseDir { get; } - public IFileSystem ParentFileSystem { get; } - - public string FullPath => BaseDir.FullPath; - public OpenDirectoryMode Mode => BaseDir.Mode; - - public ReadOnlyDirectory(IFileSystem parentFileSystem, IDirectory baseDirectory) - { - ParentFileSystem = parentFileSystem; - BaseDir = baseDirectory; - } - - public IEnumerable Read() => BaseDir.Read(); - public int GetEntryCount() => BaseDir.GetEntryCount(); - } -} diff --git a/src/LibHac/Fs/ReadOnlyFile.cs b/src/LibHac/Fs/ReadOnlyFile.cs deleted file mode 100644 index 14064046..00000000 --- a/src/LibHac/Fs/ReadOnlyFile.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class ReadOnlyFile : FileBase - { - private IFile BaseFile { get; } - - public ReadOnlyFile(IFile baseFile) - { - BaseFile = baseFile; - } - - public override int Read(Span destination, long offset, ReadOption options) - { - return BaseFile.Read(destination, offset, options); - } - - public override long GetSize() - { - return BaseFile.GetSize(); - } - - public override void Flush() { } - - public override void Write(ReadOnlySpan source, long offset, WriteOption options) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFile); - - public override void SetSize(long size) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFile); - } -} diff --git a/src/LibHac/Fs/ReadOnlyFileSystem.cs b/src/LibHac/Fs/ReadOnlyFileSystem.cs deleted file mode 100644 index 43a9d8c3..00000000 --- a/src/LibHac/Fs/ReadOnlyFileSystem.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class ReadOnlyFileSystem : IFileSystem - { - private IFileSystem BaseFs { get; } - - public ReadOnlyFileSystem(IFileSystem baseFileSystem) - { - BaseFs = baseFileSystem; - } - - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) - { - IDirectory baseDir = BaseFs.OpenDirectory(path, mode); - return new ReadOnlyDirectory(this, baseDir); - } - - public IFile OpenFile(string path, OpenMode mode) - { - IFile baseFile = BaseFs.OpenFile(path, mode); - return new ReadOnlyFile(baseFile); - } - - public DirectoryEntryType GetEntryType(string path) - { - return BaseFs.GetEntryType(path); - } - - public long GetFreeSpaceSize(string path) - { - return 0; - } - - public long GetTotalSpaceSize(string path) - { - return BaseFs.GetTotalSpaceSize(path); - } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - return BaseFs.GetFileTimeStampRaw(path); - } - - public void Commit() - { - - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) - { - BaseFs.QueryEntry(outBuffer, inBuffer, path, queryId); - } - - public void CreateDirectory(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFileSystem); - - public void CreateFile(string path, long size, CreateFileOptions options) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFileSystem); - - public void DeleteDirectory(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFileSystem); - - public void DeleteDirectoryRecursively(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFileSystem); - - public void CleanDirectoryRecursively(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFileSystem); - - public void DeleteFile(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFileSystem); - - public void RenameDirectory(string srcPath, string dstPath) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFileSystem); - - public void RenameFile(string srcPath, string dstPath) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFileSystem); - - } -} diff --git a/src/LibHac/Fs/ResultFs.cs b/src/LibHac/Fs/ResultFs.cs index 549f5d60..ae3e247d 100644 --- a/src/LibHac/Fs/ResultFs.cs +++ b/src/LibHac/Fs/ResultFs.cs @@ -11,6 +11,19 @@ public static Result InsufficientFreeSpace => new Result(ModuleFs, 30); public static Result MountNameAlreadyExists => new Result(ModuleFs, 60); + public static Result PartitionNotFound => new Result(ModuleFs, 1001); + public static Result TargetNotFound => new Result(ModuleFs, 1002); + public static Result ExternalKeyNotFound => new Result(ModuleFs, 1004); + + public static Result InvalidBufferForGameCard => new Result(ModuleFs, 2503); + public static Result GameCardNotInserted => new Result(ModuleFs, 2520); + + public static Result GameCardNotInsertedOnGetHandle => new Result(ModuleFs, 2951); + public static Result InvalidGameCardHandleOnRead => new Result(ModuleFs, 2952); + public static Result InvalidGameCardHandleOnGetCardInfo => new Result(ModuleFs, 2954); + public static Result InvalidGameCardHandleOnOpenNormalPartition => new Result(ModuleFs, 2960); + public static Result InvalidGameCardHandleOnOpenSecurePartition => new Result(ModuleFs, 2961); + public static Result NotImplemented => new Result(ModuleFs, 3001); public static Result Result3002 => new Result(ModuleFs, 3002); public static Result SaveDataPathAlreadyExists => new Result(ModuleFs, 3003); @@ -25,6 +38,7 @@ public static Result InvalidIndirectStorageSource => new Result(ModuleFs, 4023); public static Result Result4302 => new Result(ModuleFs, 4302); + public static Result InvalidSaveDataEntryType => new Result(ModuleFs, 4303); public static Result InvalidSaveDataHeader => new Result(ModuleFs, 4315); public static Result Result4362 => new Result(ModuleFs, 4362); public static Result Result4363 => new Result(ModuleFs, 4363); @@ -61,6 +75,10 @@ public static Result Result4812 => new Result(ModuleFs, 4812); + public static Result UnexpectedErrorInHostFileFlush => new Result(ModuleFs, 5307); + public static Result UnexpectedErrorInHostFileGetSize => new Result(ModuleFs, 5308); + public static Result UnknownHostFileSystemError => new Result(ModuleFs, 5309); + public static Result PreconditionViolation => new Result(ModuleFs, 6000); public static Result InvalidArgument => new Result(ModuleFs, 6001); public static Result InvalidPath => new Result(ModuleFs, 6002); @@ -77,17 +95,24 @@ public static Result InvalidSize => new Result(ModuleFs, 6062); public static Result NullArgument => new Result(ModuleFs, 6063); public static Result InvalidMountName => new Result(ModuleFs, 6065); + public static Result ExtensionSizeTooLarge => new Result(ModuleFs, 6066); + public static Result ExtensionSizeInvalid => new Result(ModuleFs, 6067); public static Result InvalidOpenModeOperation => new Result(ModuleFs, 6200); - public static Result AllowAppendRequiredForImplicitExtension => new Result(ModuleFs, 6201); + public static Result FileExtensionWithoutOpenModeAllowAppend => new Result(ModuleFs, 6201); public static Result InvalidOpenModeForRead => new Result(ModuleFs, 6202); public static Result InvalidOpenModeForWrite => new Result(ModuleFs, 6203); public static Result UnsupportedOperation => new Result(ModuleFs, 6300); - public static Result UnsupportedOperationInMemoryStorageSetSize => new Result(ModuleFs, 6316); - public static Result UnsupportedOperationInHierarchicalIvfcStorageSetSize => new Result(ModuleFs, 6304); + public static Result SubStorageNotResizable => new Result(ModuleFs, 6302); + public static Result SubStorageNotResizableMiddleOfFile => new Result(ModuleFs, 6303); + public static Result UnsupportedOperationInMemoryStorageSetSize => new Result(ModuleFs, 6304); + public static Result UnsupportedOperationInAesCtrExStorageWrite => new Result(ModuleFs, 6310); + public static Result UnsupportedOperationInHierarchicalIvfcStorageSetSize => new Result(ModuleFs, 6316); public static Result UnsupportedOperationInIndirectStorageWrite => new Result(ModuleFs, 6324); public static Result UnsupportedOperationInIndirectStorageSetSize => new Result(ModuleFs, 6325); + public static Result UnsupportedOperationInRoGameCardStorageWrite => new Result(ModuleFs, 6350); + public static Result UnsupportedOperationInRoGameCardStorageSetSize => new Result(ModuleFs, 6351); public static Result UnsupportedOperationInConcatFsQueryEntry => new Result(ModuleFs, 6359); public static Result UnsupportedOperationModifyRomFsFileSystem => new Result(ModuleFs, 6364); public static Result UnsupportedOperationRomFsFileSystemGetSpace => new Result(ModuleFs, 6366); @@ -99,12 +124,18 @@ public static Result UnsupportedOperationInPartitionFileSetSize => new Result(ModuleFs, 6376); public static Result PermissionDenied => new Result(ModuleFs, 6400); + public static Result ExternalKeyAlreadyRegistered => new Result(ModuleFs, 6452); public static Result WriteStateUnflushed => new Result(ModuleFs, 6454); public static Result WritableFileOpen => new Result(ModuleFs, 6457); + public static Result MappingTableFull => new Result(ModuleFs, 6706); public static Result AllocationTableInsufficientFreeBlocks => new Result(ModuleFs, 6707); + public static Result OpenCountLimit => new Result(ModuleFs, 6709); + public static Result RemapStorageMapFull => new Result(ModuleFs, 6811); + + public static Result SubStorageNotInitialized => new Result(ModuleFs, 6902); public static Result MountNameNotFound => new Result(ModuleFs, 6905); } } diff --git a/src/LibHac/Fs/RightsId.cs b/src/LibHac/Fs/RightsId.cs new file mode 100644 index 00000000..120eec89 --- /dev/null +++ b/src/LibHac/Fs/RightsId.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Fs +{ + [DebuggerDisplay("{DebugDisplay(),nq}")] + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + public struct RightsId : IEquatable, IComparable, IComparable + { + public readonly Id128 Id; + + public RightsId(ulong high, ulong low) + { + Id = new Id128(high, low); + } + + public RightsId(ReadOnlySpan uid) + { + Id = new Id128(uid); + } + + public override string ToString() => Id.ToString(); + + public string DebugDisplay() + { + ReadOnlySpan highBytes = AsBytes().Slice(0, 8); + ReadOnlySpan lowBytes = AsBytes().Slice(8, 8); + + return $"{highBytes.ToHexString()} {lowBytes.ToHexString()}"; + } + + public bool Equals(RightsId other) => Id == other.Id; + public override bool Equals(object obj) => obj is RightsId other && Equals(other); + + public override int GetHashCode() => Id.GetHashCode(); + + public int CompareTo(RightsId other) => Id.CompareTo(other.Id); + + public int CompareTo(object obj) + { + if (obj is null) return 1; + return obj is RightsId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(RightsId)}"); + } + + public void ToBytes(Span output) => Id.ToBytes(output); + + public ReadOnlySpan AsBytes() + { + return SpanHelpers.AsByteSpan(ref this); + } + + public static bool operator ==(RightsId left, RightsId right) => left.Equals(right); + public static bool operator !=(RightsId left, RightsId right) => !left.Equals(right); + + public static bool operator <(RightsId left, RightsId right) => left.CompareTo(right) < 0; + public static bool operator >(RightsId left, RightsId right) => left.CompareTo(right) > 0; + public static bool operator <=(RightsId left, RightsId right) => left.CompareTo(right) <= 0; + public static bool operator >=(RightsId left, RightsId right) => left.CompareTo(right) >= 0; + } +} \ No newline at end of file diff --git a/src/LibHac/Fs/RomFs/RomFsDirectory.cs b/src/LibHac/Fs/RomFs/RomFsDirectory.cs deleted file mode 100644 index 982ae423..00000000 --- a/src/LibHac/Fs/RomFs/RomFsDirectory.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; - -namespace LibHac.Fs.RomFs -{ - public class RomFsDirectory : IDirectory - { - IFileSystem IDirectory.ParentFileSystem => ParentFileSystem; - public RomFsFileSystem ParentFileSystem { get; } - public string FullPath { get; } - - public OpenDirectoryMode Mode { get; } - - private FindPosition InitialPosition { get; } - - public RomFsDirectory(RomFsFileSystem fs, string path, FindPosition position, OpenDirectoryMode mode) - { - ParentFileSystem = fs; - InitialPosition = position; - FullPath = path; - Mode = mode; - } - - public IEnumerable Read() - { - FindPosition position = InitialPosition; - HierarchicalRomFileTable tab = ParentFileSystem.FileTable; - - if (Mode.HasFlag(OpenDirectoryMode.Directories)) - { - while (tab.FindNextDirectory(ref position, out string name)) - { - yield return new DirectoryEntry(name, PathTools.Combine(FullPath, name), DirectoryEntryType.Directory, 0); - } - } - - if (Mode.HasFlag(OpenDirectoryMode.Files)) - { - while (tab.FindNextFile(ref position, out RomFileInfo info, out string name)) - { - yield return new DirectoryEntry(name, PathTools.Combine(FullPath, name), DirectoryEntryType.File, info.Length); - } - } - } - - public int GetEntryCount() - { - int count = 0; - - FindPosition position = InitialPosition; - HierarchicalRomFileTable tab = ParentFileSystem.FileTable; - - if (Mode.HasFlag(OpenDirectoryMode.Directories)) - { - while (tab.FindNextDirectory(ref position, out string _)) - { - count++; - } - } - - if (Mode.HasFlag(OpenDirectoryMode.Files)) - { - while (tab.FindNextFile(ref position, out RomFileInfo _, out string _)) - { - count++; - } - } - - return count; - } - } -} diff --git a/src/LibHac/Fs/RomFs/RomFsFile.cs b/src/LibHac/Fs/RomFs/RomFsFile.cs deleted file mode 100644 index 30e8a0c0..00000000 --- a/src/LibHac/Fs/RomFs/RomFsFile.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; - -namespace LibHac.Fs.RomFs -{ - public class RomFsFile : FileBase - { - private IStorage BaseStorage { get; } - private long Offset { get; } - private long Size { get; } - - public RomFsFile(IStorage baseStorage, long offset, long size) - { - Mode = OpenMode.Read; - BaseStorage = baseStorage; - Offset = offset; - Size = size; - } - - public override int Read(Span destination, long offset, ReadOption options) - { - int toRead = ValidateReadParamsAndGetSize(destination, offset); - - long storageOffset = Offset + offset; - BaseStorage.Read(destination.Slice(0, toRead), storageOffset); - - return toRead; - } - - public override void Write(ReadOnlySpan source, long offset, WriteOption options) - { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFile); - } - - public override void Flush() - { - } - - public override long GetSize() - { - return Size; - } - - public override void SetSize(long size) - { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFile); - } - } -} diff --git a/src/LibHac/Fs/Save/SaveDataDirectory.cs b/src/LibHac/Fs/Save/SaveDataDirectory.cs deleted file mode 100644 index 84a26b96..00000000 --- a/src/LibHac/Fs/Save/SaveDataDirectory.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; - -namespace LibHac.Fs.Save -{ - public class SaveDataDirectory : IDirectory - { - IFileSystem IDirectory.ParentFileSystem => ParentFileSystem; - public SaveDataFileSystemCore ParentFileSystem { get; } - public string FullPath { get; } - - public OpenDirectoryMode Mode { get; } - - private SaveFindPosition InitialPosition { get; } - - public SaveDataDirectory(SaveDataFileSystemCore fs, string path, SaveFindPosition position, OpenDirectoryMode mode) - { - ParentFileSystem = fs; - InitialPosition = position; - FullPath = path; - Mode = mode; - } - - public IEnumerable Read() - { - SaveFindPosition position = InitialPosition; - HierarchicalSaveFileTable tab = ParentFileSystem.FileTable; - - if (Mode.HasFlag(OpenDirectoryMode.Directories)) - { - while (tab.FindNextDirectory(ref position, out string name)) - { - yield return new DirectoryEntry(name, PathTools.Combine(FullPath, name), DirectoryEntryType.Directory, 0); - } - } - - if (Mode.HasFlag(OpenDirectoryMode.Files)) - { - while (tab.FindNextFile(ref position, out SaveFileInfo info, out string name)) - { - yield return new DirectoryEntry(name, PathTools.Combine(FullPath, name), DirectoryEntryType.File, info.Length); - } - } - } - - public int GetEntryCount() - { - int count = 0; - - SaveFindPosition position = InitialPosition; - HierarchicalSaveFileTable tab = ParentFileSystem.FileTable; - - if (Mode.HasFlag(OpenDirectoryMode.Directories)) - { - while (tab.FindNextDirectory(ref position, out string _)) - { - count++; - } - } - - if (Mode.HasFlag(OpenDirectoryMode.Files)) - { - while (tab.FindNextFile(ref position, out SaveFileInfo _, out string _)) - { - count++; - } - } - - return count; - } - } -} diff --git a/src/LibHac/Fs/Save/SaveDataFile.cs b/src/LibHac/Fs/Save/SaveDataFile.cs deleted file mode 100644 index c6ba6bf1..00000000 --- a/src/LibHac/Fs/Save/SaveDataFile.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.IO; - -namespace LibHac.Fs.Save -{ - public class SaveDataFile : FileBase - { - private AllocationTableStorage BaseStorage { get; } - private string Path { get; } - private HierarchicalSaveFileTable FileTable { get; } - private long Size { get; set; } - - public SaveDataFile(AllocationTableStorage baseStorage, string path, HierarchicalSaveFileTable fileTable, long size, OpenMode mode) - { - Mode = mode; - BaseStorage = baseStorage; - Path = path; - FileTable = fileTable; - Size = size; - } - - public override int Read(Span destination, long offset, ReadOption options) - { - int toRead = ValidateReadParamsAndGetSize(destination, offset); - - BaseStorage.Read(destination.Slice(0, toRead), offset); - - return toRead; - } - - public override void Write(ReadOnlySpan source, long offset, WriteOption options) - { - ValidateWriteParams(source, offset); - - BaseStorage.Write(source, offset); - - if ((options & WriteOption.Flush) != 0) - { - Flush(); - } - } - - public override void Flush() - { - BaseStorage.Flush(); - } - - public override long GetSize() - { - return Size; - } - - public override void SetSize(long size) - { - if (size < 0) throw new ArgumentOutOfRangeException(nameof(size)); - if (Size == size) return; - - BaseStorage.SetSize(size); - - if (!FileTable.TryOpenFile(Path, out SaveFileInfo fileInfo)) - { - throw new FileNotFoundException(); - } - - fileInfo.StartBlock = BaseStorage.InitialBlock; - fileInfo.Length = size; - - FileTable.AddFile(Path, ref fileInfo); - - Size = size; - } - } -} diff --git a/src/LibHac/Fs/SaveData.cs b/src/LibHac/Fs/SaveData.cs new file mode 100644 index 00000000..a0bc1b21 --- /dev/null +++ b/src/LibHac/Fs/SaveData.cs @@ -0,0 +1,108 @@ +using LibHac.Common; +using LibHac.FsService; + +namespace LibHac.Fs +{ + public static class SaveData + { + public static Result MountSystemSaveData(this FileSystemClient fs, U8Span mountName, SaveDataSpaceId spaceId, ulong saveDataId) + { + return MountSystemSaveData(fs, mountName, spaceId, saveDataId, UserId.Zero); + } + + public static Result MountSystemSaveData(this FileSystemClient fs, U8Span mountName, + SaveDataSpaceId spaceId, ulong saveDataId, UserId userId) + { + Result rc = MountHelpers.CheckMountName(mountName); + if (rc.IsFailure()) return rc; + + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + SaveDataAttribute attribute = default; + attribute.UserId = userId; + attribute.SaveDataId = saveDataId; + + rc = fsProxy.OpenSaveDataFileSystemBySystemSaveDataId(out IFileSystem fileSystem, spaceId, ref attribute); + if (rc.IsFailure()) return rc; + + return fs.Register(mountName, fileSystem); + } + + public static Result CreateSystemSaveData(this FileSystemClient fs, SaveDataSpaceId spaceId, + ulong saveDataId, UserId userId, ulong ownerId, long size, long journalSize, uint flags) + { + return fs.RunOperationWithAccessLog(LocalAccessLogMode.System, + () => + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + + var attribute = new SaveDataAttribute + { + UserId = userId, + SaveDataId = saveDataId + }; + + var createInfo = new SaveDataCreateInfo + { + Size = size, + JournalSize = journalSize, + BlockSize = 0x4000, + OwnerId = ownerId, + Flags = flags, + SpaceId = spaceId + }; + + return fsProxy.CreateSaveDataFileSystemBySystemSaveDataId(ref attribute, ref createInfo); + }, + () => $", savedataspaceid: {spaceId}, savedataid: 0x{saveDataId:X}, userid: 0x{userId.Id.High:X16}{userId.Id.Low:X16}, save_data_owner_id: 0x{ownerId:X}, save_data_size: {size}, save_data_journal_size: {journalSize}, save_data_flags: 0x{flags:X8}"); + } + + public static Result CreateSystemSaveData(this FileSystemClient fs, ulong saveDataId, UserId userId, + ulong ownerId, long size, long journalSize, uint flags) + { + return CreateSystemSaveData(fs, SaveDataSpaceId.System, saveDataId, userId, ownerId, size, journalSize, flags); + } + + public static Result CreateSystemSaveData(this FileSystemClient fs, ulong saveDataId, UserId userId, long size, + long journalSize, uint flags) + { + return CreateSystemSaveData(fs, SaveDataSpaceId.System, saveDataId, userId, 0, size, journalSize, flags); + } + + public static Result CreateSystemSaveData(this FileSystemClient fs, ulong saveDataId, ulong ownerId, long size, + long journalSize, uint flags) + { + return CreateSystemSaveData(fs, SaveDataSpaceId.System, saveDataId, UserId.Zero, ownerId, size, journalSize, flags); + } + + public static Result CreateSystemSaveData(this FileSystemClient fs, ulong saveDataId, long size, + long journalSize, uint flags) + { + return CreateSystemSaveData(fs, SaveDataSpaceId.System, saveDataId, UserId.Zero, 0, size, journalSize, flags); + } + + public static Result CreateSystemSaveData(this FileSystemClient fs, SaveDataSpaceId spaceId, ulong saveDataId, + ulong ownerId, long size, long journalSize, uint flags) + { + return CreateSystemSaveData(fs, spaceId, saveDataId, UserId.Zero, ownerId, size, journalSize, flags); + } + + public static Result DeleteSaveData(this FileSystemClient fs, ulong saveDataId) + { + return fs.RunOperationWithAccessLog(LocalAccessLogMode.System, + () => + { + IFileSystemProxy fsProxy = fs.GetFileSystemProxyServiceObject(); + return fsProxy.DeleteSaveDataFileSystem(saveDataId); + }, + () => $", savedataid: 0x{saveDataId:X}"); + } + + public static Result DisableAutoSaveDataCreation(this FileSystemClient fsClient) + { + IFileSystemProxy fsProxy = fsClient.GetFileSystemProxyServiceObject(); + + return fsProxy.DisableAutoSaveDataCreation(); + } + } +} diff --git a/src/LibHac/Fs/SaveDataStruct.cs b/src/LibHac/Fs/SaveDataAttributeKvdb.cs similarity index 87% rename from src/LibHac/Fs/SaveDataStruct.cs rename to src/LibHac/Fs/SaveDataAttributeKvdb.cs index a0a71f49..5588bdf2 100644 --- a/src/LibHac/Fs/SaveDataStruct.cs +++ b/src/LibHac/Fs/SaveDataAttributeKvdb.cs @@ -1,11 +1,11 @@ using System; using System.Buffers.Binary; -using LibHac.Fs.Save; +using LibHac.FsSystem.Save; using LibHac.Kvdb; namespace LibHac.Fs { - public class SaveDataStruct : IComparable, IComparable, IEquatable, IExportable + public class SaveDataAttributeKvdb : IComparable, IComparable, IEquatable, IExportable { public ulong TitleId { get; private set; } public UserId UserId { get; private set; } @@ -44,7 +44,7 @@ namespace LibHac.Fs public void Freeze() => _isFrozen = true; - public bool Equals(SaveDataStruct other) + public bool Equals(SaveDataAttributeKvdb other) { return other != null && TitleId == other.TitleId && UserId.Equals(other.UserId) && SaveId == other.SaveId && Type == other.Type && Rank == other.Rank && Index == other.Index; @@ -52,7 +52,7 @@ namespace LibHac.Fs public override bool Equals(object obj) { - return obj is SaveDataStruct other && Equals(other); + return obj is SaveDataAttributeKvdb other && Equals(other); } public override int GetHashCode() @@ -71,7 +71,7 @@ namespace LibHac.Fs } } - public int CompareTo(SaveDataStruct other) + public int CompareTo(SaveDataAttributeKvdb other) { int titleIdComparison = TitleId.CompareTo(other.TitleId); if (titleIdComparison != 0) return titleIdComparison; @@ -89,7 +89,7 @@ namespace LibHac.Fs public int CompareTo(object obj) { if (obj is null) return 1; - return obj is SaveDataStruct other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(SaveDataStruct)}"); + return obj is SaveDataAttributeKvdb other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(SaveDataAttributeKvdb)}"); } } } diff --git a/src/LibHac/Fs/SaveDataStructs.cs b/src/LibHac/Fs/SaveDataStructs.cs new file mode 100644 index 00000000..581dc4cd --- /dev/null +++ b/src/LibHac/Fs/SaveDataStructs.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.FsSystem.Save; +using LibHac.Ncm; + +namespace LibHac.Fs +{ + [StructLayout(LayoutKind.Explicit, Size = 0x40)] + public struct SaveDataAttribute + { + [FieldOffset(0x00)] public ulong TitleId; + [FieldOffset(0x08)] public UserId UserId; + [FieldOffset(0x18)] public ulong SaveDataId; + [FieldOffset(0x20)] public SaveDataType Type; + [FieldOffset(0x21)] public byte Rank; + [FieldOffset(0x22)] public short Index; + } + + [StructLayout(LayoutKind.Explicit, Size = 0x48)] + public struct SaveDataFilter + { + [FieldOffset(0x00)] public bool FilterByTitleId; + [FieldOffset(0x01)] public bool FilterBySaveDataType; + [FieldOffset(0x02)] public bool FilterByUserId; + [FieldOffset(0x03)] public bool FilterBySaveDataId; + [FieldOffset(0x04)] public bool FilterByIndex; + [FieldOffset(0x05)] public byte Rank; + + [FieldOffset(0x08)] public TitleId TitleID; + [FieldOffset(0x10)] public UserId UserId; + [FieldOffset(0x20)] public ulong SaveDataId; + [FieldOffset(0x28)] public SaveDataType SaveDataType; + [FieldOffset(0x2A)] public short Index; + } + + [StructLayout(LayoutKind.Explicit, Size = 0x50)] + public struct SaveDataFilterInternal + { + [FieldOffset(0x00)] public bool FilterBySaveDataSpaceId; + [FieldOffset(0x01)] public SaveDataSpaceId SpaceId; + + [FieldOffset(0x08)] public bool FilterByTitleId; + [FieldOffset(0x10)] public TitleId TitleID; + + [FieldOffset(0x18)] public bool FilterBySaveDataType; + [FieldOffset(0x19)] public SaveDataType SaveDataType; + + [FieldOffset(0x20)] public bool FilterByUserId; + [FieldOffset(0x28)] public UserId UserId; + + [FieldOffset(0x38)] public bool FilterBySaveDataId; + [FieldOffset(0x40)] public ulong SaveDataId; + + [FieldOffset(0x48)] public bool FilterByIndex; + [FieldOffset(0x4A)] public short Index; + + [FieldOffset(0x4C)] public int Rank; + } + + [StructLayout(LayoutKind.Explicit, Size = HashLength)] + public struct HashSalt + { + private const int HashLength = 0x20; + + [FieldOffset(0x00)] private byte _hashStart; + + public Span Hash => SpanHelpers.CreateSpan(ref _hashStart, HashLength); + } + + [StructLayout(LayoutKind.Explicit, Size = 0x10)] + public struct SaveMetaCreateInfo + { + [FieldOffset(0)] public int Size; + [FieldOffset(4)] public SaveMetaType Type; + } + + [StructLayout(LayoutKind.Explicit, Size = 0x40)] + public struct SaveDataCreateInfo + { + [FieldOffset(0x00)] public long Size; + [FieldOffset(0x08)] public long JournalSize; + [FieldOffset(0x10)] public ulong BlockSize; + [FieldOffset(0x18)] public ulong OwnerId; + [FieldOffset(0x20)] public uint Flags; + [FieldOffset(0x24)] public SaveDataSpaceId SpaceId; + [FieldOffset(0x25)] public bool Field25; + } +} diff --git a/src/LibHac/Fs/SdCardAccessLog.cs b/src/LibHac/Fs/SdCardAccessLog.cs new file mode 100644 index 00000000..345e64c2 --- /dev/null +++ b/src/LibHac/Fs/SdCardAccessLog.cs @@ -0,0 +1,16 @@ +using System; +using LibHac.FsService; + +namespace LibHac.Fs +{ + /// + /// The default access logger that will output to the SD card via . + /// + public class SdCardAccessLog : IAccessLog + { + public void Log(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, string message, string caller = "") + { + throw new NotImplementedException(); + } + } +} diff --git a/src/LibHac/Fs/SectorStorage.cs b/src/LibHac/Fs/SectorStorage.cs deleted file mode 100644 index 7e3efee7..00000000 --- a/src/LibHac/Fs/SectorStorage.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class SectorStorage : StorageBase - { - protected IStorage BaseStorage { get; } - - public int SectorSize { get; } - public int SectorCount { get; private set; } - - private long _length; - - public SectorStorage(IStorage baseStorage, int sectorSize, bool leaveOpen) - { - BaseStorage = baseStorage; - SectorSize = sectorSize; - SectorCount = (int)Util.DivideByRoundUp(BaseStorage.GetSize(), SectorSize); - _length = BaseStorage.GetSize(); - - if (!leaveOpen) ToDispose.Add(BaseStorage); - } - - protected override void ReadImpl(Span destination, long offset) - { - ValidateSize(destination.Length, offset); - BaseStorage.Read(destination, offset); - } - - protected override void WriteImpl(ReadOnlySpan source, long offset) - { - ValidateSize(source.Length, offset); - BaseStorage.Write(source, offset); - } - - public override void Flush() - { - BaseStorage.Flush(); - } - - public override long GetSize() => _length; - - public override void SetSize(long size) - { - BaseStorage.SetSize(size); - - SectorCount = (int)Util.DivideByRoundUp(BaseStorage.GetSize(), SectorSize); - _length = BaseStorage.GetSize(); - } - - /// - /// Validates that the size is a multiple of the sector size - /// - protected void ValidateSize(long size, long offset) - { - if (size < 0) - throw new ArgumentException("Size must be non-negative"); - if (offset < 0) - throw new ArgumentException("Offset must be non-negative"); - if (offset % SectorSize != 0) - throw new ArgumentException($"Offset must be a multiple of {SectorSize}"); - } - } -} diff --git a/src/LibHac/Fs/StorageBase.cs b/src/LibHac/Fs/StorageBase.cs index fe42a6b3..f764494f 100644 --- a/src/LibHac/Fs/StorageBase.cs +++ b/src/LibHac/Fs/StorageBase.cs @@ -1,69 +1,85 @@ using System; -using System.Collections.Generic; +using System.Threading; namespace LibHac.Fs { public abstract class StorageBase : IStorage { - private bool _isDisposed; - protected internal List ToDispose { get; } = new List(); - protected bool CanAutoExpand { get; set; } + // 0 = not disposed; 1 = disposed + private int _disposedState; + private bool IsDisposed => _disposedState != 0; - protected abstract void ReadImpl(Span destination, long offset); - protected abstract void WriteImpl(ReadOnlySpan source, long offset); - public abstract void Flush(); - public abstract long GetSize(); + protected abstract Result ReadImpl(long offset, Span destination); + protected abstract Result WriteImpl(long offset, ReadOnlySpan source); + protected abstract Result FlushImpl(); + protected abstract Result GetSizeImpl(out long size); + protected abstract Result SetSizeImpl(long size); - public void Read(Span destination, long offset) + protected virtual Result OperateRangeImpl(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) { - ValidateParameters(destination, offset); - ReadImpl(destination, offset); + return ResultFs.NotImplemented.Log(); } - public void Write(ReadOnlySpan source, long offset) + public Result Read(long offset, Span destination) { - ValidateParameters(source, offset); - WriteImpl(source, offset); + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return ReadImpl(offset, destination); } - public virtual void SetSize(long size) + public Result Write(long offset, ReadOnlySpan source) { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return WriteImpl(offset, source); } - protected virtual void Dispose(bool disposing) + public Result Flush() { - if (_isDisposed) return; + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); - if (disposing) - { - Flush(); - foreach (IDisposable item in ToDispose) - { - item?.Dispose(); - } - } + return FlushImpl(); + } - _isDisposed = true; + public Result SetSize(long size) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return SetSizeImpl(size); + } + + public Result GetSize(out long size) + { + size = default; + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return GetSizeImpl(out size); + } + + public Result OperateRange(Span outBuffer, OperationId operationId, long offset, long size, + ReadOnlySpan inBuffer) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return OperateRange(outBuffer, operationId, offset, size, inBuffer); } public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); + // Make sure Dispose is only called once + if (Interlocked.CompareExchange(ref _disposedState, 1, 0) == 0) + { + Dispose(true); + GC.SuppressFinalize(this); + } } - protected void ValidateParameters(ReadOnlySpan span, long offset) + protected virtual void Dispose(bool disposing) { } + + public static bool IsRangeValid(long offset, long size, long totalSize) { - if (_isDisposed) throw new ObjectDisposedException(null); - if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), "Argument must be non-negative."); - - long length = GetSize(); - - if (length != -1 && !CanAutoExpand) - { - if (offset + span.Length > length) throw new ArgumentException("The given offset and count exceed the length of the Storage"); - } + return offset >= 0 && size >= 0 && size <= totalSize && offset <= totalSize - size; } } } diff --git a/src/LibHac/Fs/StorageFile.cs b/src/LibHac/Fs/StorageFile.cs deleted file mode 100644 index ce362ebe..00000000 --- a/src/LibHac/Fs/StorageFile.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class StorageFile : FileBase - { - private IStorage BaseStorage { get; } - - public StorageFile(IStorage baseStorage, OpenMode mode) - { - BaseStorage = baseStorage; - Mode = mode; - } - - public override int Read(Span destination, long offset, ReadOption options) - { - int toRead = ValidateReadParamsAndGetSize(destination, offset); - - BaseStorage.Read(destination.Slice(0, toRead), offset); - - return toRead; - } - - public override void Write(ReadOnlySpan source, long offset, WriteOption options) - { - ValidateWriteParams(source, offset); - - BaseStorage.Write(source, offset); - - if ((options & WriteOption.Flush) != 0) - { - Flush(); - } - } - - public override void Flush() - { - BaseStorage.Flush(); - } - - public override long GetSize() - { - return BaseStorage.GetSize(); - } - - public override void SetSize(long size) - { - BaseStorage.SetSize(size); - } - } -} diff --git a/src/LibHac/Fs/SubStorage.cs b/src/LibHac/Fs/SubStorage.cs deleted file mode 100644 index 710cce14..00000000 --- a/src/LibHac/Fs/SubStorage.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.IO; - -namespace LibHac.Fs -{ - public class SubStorage : StorageBase - { - private IStorage BaseStorage { get; } - private long Offset { get; } - private FileAccess Access { get; } = FileAccess.ReadWrite; - private long _length; - - public SubStorage(IStorage baseStorage, long offset, long length) - { - BaseStorage = baseStorage; - Offset = offset; - _length = length; - } - - public SubStorage(SubStorage baseStorage, long offset, long length) - { - BaseStorage = baseStorage.BaseStorage; - Offset = baseStorage.Offset + offset; - _length = length; - } - - public SubStorage(IStorage baseStorage, long offset, long length, bool leaveOpen) - : this(baseStorage, offset, length) - { - if (!leaveOpen) ToDispose.Add(BaseStorage); - } - - public SubStorage(IStorage baseStorage, long offset, long length, bool leaveOpen, FileAccess access) - : this(baseStorage, offset, length, leaveOpen) - { - Access = access; - } - - protected override void ReadImpl(Span destination, long offset) - { - if ((Access & FileAccess.Read) == 0) throw new InvalidOperationException("Storage is not readable"); - BaseStorage.Read(destination, offset + Offset); - } - - protected override void WriteImpl(ReadOnlySpan source, long offset) - { - if ((Access & FileAccess.Write) == 0) throw new InvalidOperationException("Storage is not writable"); - BaseStorage.Write(source, offset + Offset); - } - - public override void Flush() - { - BaseStorage.Flush(); - } - - public override long GetSize() => _length; - - public override void SetSize(long size) - { - //if (!IsResizable) - // return 0x313802; - - //if (Offset < 0 || size < 0) - // return 0x2F5C02; - - if (BaseStorage.GetSize() != Offset + _length) - { - throw new NotSupportedException("SubStorage cannot be resized unless it is located at the end of the base storage."); - } - - BaseStorage.SetSize(Offset + size); - - _length = size; - } - } -} diff --git a/src/LibHac/Fs/SubStorage2.cs b/src/LibHac/Fs/SubStorage2.cs new file mode 100644 index 00000000..2cbe06a1 --- /dev/null +++ b/src/LibHac/Fs/SubStorage2.cs @@ -0,0 +1,85 @@ +using System; + +namespace LibHac.Fs +{ + public class SubStorage2 : StorageBase + { + private IStorage BaseStorage { get; } + private long Offset { get; } + private long Size { get; set; } + public bool IsResizable { get; set; } + + public SubStorage2(IStorage baseStorage, long offset, long size) + { + BaseStorage = baseStorage; + Offset = offset; + Size = size; + } + + public SubStorage2(SubStorage2 baseStorage, long offset, long size) + { + BaseStorage = baseStorage.BaseStorage; + Offset = baseStorage.Offset + offset; + Size = size; + } + + protected override Result ReadImpl(long offset, Span destination) + { + if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + if (destination.Length == 0) return Result.Success; + + if (!IsRangeValid(offset, destination.Length, Size)) return ResultFs.ValueOutOfRange.Log(); + + return BaseStorage.Read(Offset + offset, destination); + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source) + { + if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + if (source.Length == 0) return Result.Success; + + if (!IsRangeValid(offset, source.Length, Size)) return ResultFs.ValueOutOfRange.Log(); + + return BaseStorage.Write(Offset + offset, source); + } + + protected override Result FlushImpl() + { + if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + + return BaseStorage.Flush(); + } + + protected override Result SetSizeImpl(long size) + { + if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + if (!IsResizable) return ResultFs.SubStorageNotResizable.Log(); + if (size < 0 || Offset < 0) return ResultFs.InvalidSize.Log(); + + Result rc = BaseStorage.GetSize(out long baseSize); + if (rc.IsFailure()) return rc; + + if (baseSize != Offset + Size) + { + // SubStorage cannot be resized unless it is located at the end of the base storage. + return ResultFs.SubStorageNotResizableMiddleOfFile.Log(); + } + + rc = BaseStorage.SetSize(Offset + size); + if (rc.IsFailure()) return rc; + + Size = size; + return Result.Success; + } + + protected override Result GetSizeImpl(out long size) + { + size = default; + + if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + + size = Size; + return Result.Success; + } + } +} diff --git a/src/LibHac/Fs/SubdirectoryFileSystem.cs b/src/LibHac/Fs/SubdirectoryFileSystem.cs deleted file mode 100644 index db5f0ab5..00000000 --- a/src/LibHac/Fs/SubdirectoryFileSystem.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; - -namespace LibHac.Fs -{ - public class SubdirectoryFileSystem : IFileSystem - { - private string RootPath { get; } - private IFileSystem ParentFileSystem { get; } - - private string ResolveFullPath(string path) - { - return PathTools.Combine(RootPath, path); - } - - public SubdirectoryFileSystem(IFileSystem fs, string rootPath) - { - ParentFileSystem = fs; - RootPath = PathTools.Normalize(rootPath); - } - - public void CreateDirectory(string path) - { - path = PathTools.Normalize(path); - - ParentFileSystem.CreateDirectory(ResolveFullPath(path)); - } - - public void CreateFile(string path, long size, CreateFileOptions options) - { - path = PathTools.Normalize(path); - - ParentFileSystem.CreateFile(ResolveFullPath(path), size, options); - } - - public void DeleteDirectory(string path) - { - path = PathTools.Normalize(path); - - ParentFileSystem.DeleteDirectory(ResolveFullPath(path)); - } - - public void DeleteDirectoryRecursively(string path) - { - path = PathTools.Normalize(path); - - ParentFileSystem.DeleteDirectoryRecursively(ResolveFullPath(path)); - } - - public void CleanDirectoryRecursively(string path) - { - path = PathTools.Normalize(path); - - ParentFileSystem.CleanDirectoryRecursively(ResolveFullPath(path)); - } - - public void DeleteFile(string path) - { - path = PathTools.Normalize(path); - - ParentFileSystem.DeleteFile(ResolveFullPath(path)); - } - - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) - { - path = PathTools.Normalize(path); - - IDirectory baseDir = ParentFileSystem.OpenDirectory(ResolveFullPath(path), mode); - - return new SubdirectoryFileSystemDirectory(this, baseDir, path, mode); - } - - public IFile OpenFile(string path, OpenMode mode) - { - path = PathTools.Normalize(path); - - return ParentFileSystem.OpenFile(ResolveFullPath(path), mode); - } - - public void RenameDirectory(string srcPath, string dstPath) - { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); - - ParentFileSystem.RenameDirectory(ResolveFullPath(srcPath), ResolveFullPath(dstPath)); - } - - public void RenameFile(string srcPath, string dstPath) - { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); - - ParentFileSystem.RenameFile(ResolveFullPath(srcPath), ResolveFullPath(dstPath)); - } - - public DirectoryEntryType GetEntryType(string path) - { - path = PathTools.Normalize(path); - - return ParentFileSystem.GetEntryType(ResolveFullPath(path)); - } - - public void Commit() - { - ParentFileSystem.Commit(); - } - - public long GetFreeSpaceSize(string path) - { - path = PathTools.Normalize(path); - - return ParentFileSystem.GetFreeSpaceSize(ResolveFullPath(path)); - } - - public long GetTotalSpaceSize(string path) - { - path = PathTools.Normalize(path); - - return ParentFileSystem.GetTotalSpaceSize(ResolveFullPath(path)); - } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - path = PathTools.Normalize(path); - - return ParentFileSystem.GetFileTimeStampRaw(ResolveFullPath(path)); - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) - { - path = PathTools.Normalize(path); - - ParentFileSystem.QueryEntry(outBuffer, inBuffer, ResolveFullPath(path), queryId); - } - } -} diff --git a/src/LibHac/Fs/SubdirectoryFileSystemDirectory.cs b/src/LibHac/Fs/SubdirectoryFileSystemDirectory.cs deleted file mode 100644 index cc2762d6..00000000 --- a/src/LibHac/Fs/SubdirectoryFileSystemDirectory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; - -namespace LibHac.Fs -{ - public class SubdirectoryFileSystemDirectory : IDirectory - { - public SubdirectoryFileSystemDirectory(SubdirectoryFileSystem fs, IDirectory baseDir, string path, OpenDirectoryMode mode) - { - ParentFileSystem = fs; - BaseDirectory = baseDir; - FullPath = path; - Mode = mode; - } - - public IFileSystem ParentFileSystem { get; } - public string FullPath { get; } - public OpenDirectoryMode Mode { get; } - - private IDirectory BaseDirectory { get; } - - public IEnumerable Read() - { - foreach (DirectoryEntry entry in BaseDirectory.Read()) - { - yield return new DirectoryEntry(entry.Name, PathTools.Combine(FullPath, entry.Name), entry.Type, entry.Size); - } - } - - public int GetEntryCount() - { - return BaseDirectory.GetEntryCount(); - } - } -} diff --git a/src/LibHac/Fs/UserId.cs b/src/LibHac/Fs/UserId.cs index 3045249b..31db1dec 100644 --- a/src/LibHac/Fs/UserId.cs +++ b/src/LibHac/Fs/UserId.cs @@ -1,66 +1,59 @@ using System; +using System.Diagnostics; using System.Runtime.InteropServices; +using LibHac.Common; namespace LibHac.Fs { + [DebuggerDisplay("{ToString(),nq}")] + [StructLayout(LayoutKind.Sequential, Size = 0x10)] public struct UserId : IEquatable, IComparable, IComparable { - public readonly ulong High; - public readonly ulong Low; + public static UserId Zero => default; + + public readonly Id128 Id; public UserId(ulong high, ulong low) { - High = high; - Low = low; + Id = new Id128(high, low); } public UserId(ReadOnlySpan uid) { - ReadOnlySpan longs = MemoryMarshal.Cast(uid); - - High = longs[0]; - Low = longs[1]; + Id = new Id128(uid); } - public bool Equals(UserId other) + public override string ToString() { - return High == other.High && Low == other.Low; + return $"0x{Id.High:x8}{Id.Low:x8}"; } - public override bool Equals(object obj) - { - return obj is UserId other && Equals(other); - } + public bool Equals(UserId other) => Id == other.Id; + public override bool Equals(object obj) => obj is UserId other && Equals(other); - public override int GetHashCode() - { - unchecked - { - return (High.GetHashCode() * 397) ^ Low.GetHashCode(); - } - } + public override int GetHashCode() => Id.GetHashCode(); - public int CompareTo(UserId other) - { - // ReSharper disable ImpureMethodCallOnReadonlyValueField - int highComparison = High.CompareTo(other.High); - if (highComparison != 0) return highComparison; - return Low.CompareTo(other.Low); - // ReSharper restore ImpureMethodCallOnReadonlyValueField - } + public int CompareTo(UserId other) => Id.CompareTo(other.Id); public int CompareTo(object obj) { - if (ReferenceEquals(null, obj)) return 1; + if (obj is null) return 1; return obj is UserId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(UserId)}"); } - public void ToBytes(Span output) - { - Span longs = MemoryMarshal.Cast(output); + public void ToBytes(Span output) => Id.ToBytes(output); - longs[0] = High; - longs[1] = Low; + public ReadOnlySpan AsBytes() + { + return SpanHelpers.AsByteSpan(ref this); } + + public static bool operator ==(UserId left, UserId right) => left.Equals(right); + public static bool operator !=(UserId left, UserId right) => !left.Equals(right); + + public static bool operator <(UserId left, UserId right) => left.CompareTo(right) < 0; + public static bool operator >(UserId left, UserId right) => left.CompareTo(right) > 0; + public static bool operator <=(UserId left, UserId right) => left.CompareTo(right) <= 0; + public static bool operator >=(UserId left, UserId right) => left.CompareTo(right) >= 0; } } diff --git a/src/LibHac/FsService/Creators/EmulatedBisFileSystemCreator.cs b/src/LibHac/FsService/Creators/EmulatedBisFileSystemCreator.cs new file mode 100644 index 00000000..edba44ca --- /dev/null +++ b/src/LibHac/FsService/Creators/EmulatedBisFileSystemCreator.cs @@ -0,0 +1,99 @@ +using System.Diagnostics; +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public class EmulatedBisFileSystemCreator : IBuiltInStorageFileSystemCreator + { + private EmulatedBisFileSystemCreatorConfig Config { get; } + + public EmulatedBisFileSystemCreator(IFileSystem rootFileSystem) + { + Config = new EmulatedBisFileSystemCreatorConfig(); + Config.RootFileSystem = rootFileSystem; + } + + public EmulatedBisFileSystemCreator(EmulatedBisFileSystemCreatorConfig config) + { + Config = config; + } + + public Result Create(out IFileSystem fileSystem, string rootPath, BisPartitionId partitionId) + { + fileSystem = default; + + if (!IsValidPartitionId(partitionId)) return ResultFs.InvalidArgument; + if (rootPath == null) return ResultFs.NullArgument; + + if (Config.TryGetFileSystem(out fileSystem, partitionId)) + { + return Result.Success; + } + + if (Config.RootFileSystem == null) + { + return ResultFs.PreconditionViolation; + } + + string partitionPath = GetPartitionPath(partitionId); + + Result rc = + Util.CreateSubFileSystem(out IFileSystem subFileSystem, Config.RootFileSystem, partitionPath, true); + + if (rc.IsFailure()) return rc; + + if (rootPath == string.Empty) + { + fileSystem = subFileSystem; + return Result.Success; + } + + return Util.CreateSubFileSystemImpl(out fileSystem, subFileSystem, rootPath); + } + + public Result CreateFatFileSystem(out IFileSystem fileSystem, BisPartitionId partitionId) + { + fileSystem = default; + return ResultFs.NotImplemented; + } + + public Result SetBisRootForHost(BisPartitionId partitionId, string rootPath) + { + return Config.SetPath(rootPath, partitionId); + } + + private bool IsValidPartitionId(BisPartitionId id) + { + return id >= BisPartitionId.CalibrationFile && id <= BisPartitionId.System; + } + + private string GetPartitionPath(BisPartitionId id) + { + if (Config.TryGetPartitionPath(out string path, id)) + { + return path; + } + + return GetDefaultPartitionPath(id); + } + + private string GetDefaultPartitionPath(BisPartitionId id) + { + Debug.Assert(IsValidPartitionId(id)); + + switch (id) + { + case BisPartitionId.CalibrationFile: + return "/bis/cal"; + case BisPartitionId.SafeMode: + return "/bis/safe"; + case BisPartitionId.User: + return "/bis/user"; + case BisPartitionId.System: + return "/bis/system"; + default: + return string.Empty; + } + } + } +} diff --git a/src/LibHac/FsService/Creators/EmulatedBisFileSystemCreatorConfig.cs b/src/LibHac/FsService/Creators/EmulatedBisFileSystemCreatorConfig.cs new file mode 100644 index 00000000..1e6ebe98 --- /dev/null +++ b/src/LibHac/FsService/Creators/EmulatedBisFileSystemCreatorConfig.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public class EmulatedBisFileSystemCreatorConfig + { + private const int ValidPartitionCount = 4; + + public IFileSystem RootFileSystem { get; set; } + + private IFileSystem[] PartitionFileSystems { get; } = new IFileSystem[ValidPartitionCount]; + private string[] PartitionPaths { get; } = new string[ValidPartitionCount]; + + public Result SetFileSystem(IFileSystem fileSystem, BisPartitionId partitionId) + { + if (fileSystem == null) return ResultFs.NullArgument; + if (!IsValidPartitionId(partitionId)) return ResultFs.InvalidArgument; + + PartitionFileSystems[GetArrayIndex(partitionId)] = fileSystem; + + return Result.Success; + } + + public Result SetPath(string path, BisPartitionId partitionId) + { + if (path == null) return ResultFs.NullArgument; + if (!IsValidPartitionId(partitionId)) return ResultFs.InvalidArgument; + + PartitionPaths[GetArrayIndex(partitionId)] = path; + + return Result.Success; + } + + public bool TryGetFileSystem(out IFileSystem fileSystem, BisPartitionId partitionId) + { + if (!IsValidPartitionId(partitionId)) + { + fileSystem = default; + return false; + } + + fileSystem = PartitionFileSystems[GetArrayIndex(partitionId)]; + + return fileSystem != null; + } + + public bool TryGetPartitionPath(out string path, BisPartitionId partitionId) + { + if (!IsValidPartitionId(partitionId)) + { + path = default; + return false; + } + + path = PartitionPaths[GetArrayIndex(partitionId)]; + + return path != null; + } + + private int GetArrayIndex(BisPartitionId id) + { + Debug.Assert(IsValidPartitionId(id)); + + return id - BisPartitionId.CalibrationFile; + } + + private bool IsValidPartitionId(BisPartitionId id) + { + return id >= BisPartitionId.CalibrationFile && id <= BisPartitionId.System; + } + } +} diff --git a/src/LibHac/FsService/Creators/EmulatedGameCardFsCreator.cs b/src/LibHac/FsService/Creators/EmulatedGameCardFsCreator.cs new file mode 100644 index 00000000..f5fc1f71 --- /dev/null +++ b/src/LibHac/FsService/Creators/EmulatedGameCardFsCreator.cs @@ -0,0 +1,34 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public class EmulatedGameCardFsCreator : IGameCardFileSystemCreator + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private EmulatedGameCardStorageCreator StorageCreator { get; } + private EmulatedGameCard GameCard { get; } + + public EmulatedGameCardFsCreator(EmulatedGameCardStorageCreator storageCreator, EmulatedGameCard gameCard) + { + StorageCreator = storageCreator; + GameCard = gameCard; + } + public Result Create(out IFileSystem fileSystem, GameCardHandle handle, GameCardPartition partitionType) + { + // Use the old xci code temporarily + + fileSystem = default; + + Result rc = GameCard.GetXci(out Xci xci, handle); + if (rc.IsFailure()) return rc; + + if (!xci.HasPartition((XciPartitionType)partitionType)) + { + return ResultFs.PartitionNotFound.Log(); + } + + fileSystem = xci.OpenPartition((XciPartitionType)partitionType); + return Result.Success; + } + } +} diff --git a/src/LibHac/FsService/Creators/EmulatedGameCardStorageCreator.cs b/src/LibHac/FsService/Creators/EmulatedGameCardStorageCreator.cs new file mode 100644 index 00000000..c8dc4bb7 --- /dev/null +++ b/src/LibHac/FsService/Creators/EmulatedGameCardStorageCreator.cs @@ -0,0 +1,125 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public class EmulatedGameCardStorageCreator : IGameCardStorageCreator + { + private EmulatedGameCard GameCard { get; } + + public EmulatedGameCardStorageCreator(EmulatedGameCard gameCard) + { + GameCard = gameCard; + } + + public Result CreateNormal(GameCardHandle handle, out IStorage storage) + { + storage = default; + + if (GameCard.IsGameCardHandleInvalid(handle)) + { + return ResultFs.InvalidGameCardHandleOnOpenNormalPartition.Log(); + } + + var baseStorage = new ReadOnlyGameCardStorage(GameCard, handle); + + Result rc = GameCard.GetCardInfo(out GameCardInfo cardInfo, handle); + if (rc.IsFailure()) return rc; + + storage = new SubStorage2(baseStorage, 0, cardInfo.SecureAreaOffset); + return Result.Success; + } + + public Result CreateSecure(GameCardHandle handle, out IStorage storage) + { + storage = default; + + if (GameCard.IsGameCardHandleInvalid(handle)) + { + return ResultFs.InvalidGameCardHandleOnOpenSecurePartition.Log(); + } + + Span deviceId = stackalloc byte[0x10]; + Span imageHash = stackalloc byte[0x20]; + + Result rc = GameCard.GetGameCardDeviceId(deviceId); + if (rc.IsFailure()) return rc; + + rc = GameCard.GetGameCardImageHash(imageHash); + if (rc.IsFailure()) return rc; + + var baseStorage = new ReadOnlyGameCardStorage(GameCard, handle, deviceId, imageHash); + + rc = GameCard.GetCardInfo(out GameCardInfo cardInfo, handle); + if (rc.IsFailure()) return rc; + + storage = new SubStorage2(baseStorage, cardInfo.SecureAreaOffset, cardInfo.SecureAreaSize); + return Result.Success; + } + + public Result CreateWritable(GameCardHandle handle, out IStorage storage) + { + throw new NotImplementedException(); + } + + private class ReadOnlyGameCardStorage : StorageBase + { + private EmulatedGameCard GameCard { get; } + private GameCardHandle Handle { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private bool IsSecureMode { get; } + private byte[] DeviceId { get; } = new byte[0x10]; + private byte[] ImageHash { get; } = new byte[0x20]; + + public ReadOnlyGameCardStorage(EmulatedGameCard gameCard, GameCardHandle handle) + { + GameCard = gameCard; + Handle = handle; + } + + public ReadOnlyGameCardStorage(EmulatedGameCard gameCard, GameCardHandle handle, ReadOnlySpan deviceId, ReadOnlySpan imageHash) + { + GameCard = gameCard; + Handle = handle; + IsSecureMode = true; + deviceId.CopyTo(DeviceId); + imageHash.CopyTo(ImageHash); + } + + protected override Result ReadImpl(long offset, Span destination) + { + // In secure mode, if Handle is old and the card's device ID and + // header hash are still the same, Handle is updated to the new handle + + return GameCard.Read(Handle, offset, destination); + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source) + { + return ResultFs.UnsupportedOperationInRoGameCardStorageWrite.Log(); + } + + protected override Result FlushImpl() + { + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.UnsupportedOperationInRoGameCardStorageSetSize.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + size = 0; + + Result rc = GameCard.GetCardInfo(out GameCardInfo info, Handle); + if (rc.IsFailure()) return rc; + + size = info.Size; + return Result.Success; + } + } + } +} diff --git a/src/LibHac/FsService/Creators/EmulatedSdFileSystemCreator.cs b/src/LibHac/FsService/Creators/EmulatedSdFileSystemCreator.cs new file mode 100644 index 00000000..8000510d --- /dev/null +++ b/src/LibHac/FsService/Creators/EmulatedSdFileSystemCreator.cs @@ -0,0 +1,48 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + class EmulatedSdFileSystemCreator : ISdFileSystemCreator + { + private const string DefaultPath = "/sdcard"; + + public IFileSystem RootFileSystem { get; set; } + public string Path { get; set; } + + public IFileSystem SdCardFileSystem { get; set; } + + public EmulatedSdFileSystemCreator(IFileSystem rootFileSystem) + { + RootFileSystem = rootFileSystem; + } + + public Result Create(out IFileSystem fileSystem) + { + fileSystem = default; + + if (SdCardFileSystem != null) + { + fileSystem = SdCardFileSystem; + + return Result.Success; + } + + if (RootFileSystem == null) + { + return ResultFs.PreconditionViolation; + } + + string path = Path ?? DefaultPath; + + // Todo: Add ProxyFileSystem? + + return Util.CreateSubFileSystem(out fileSystem, RootFileSystem, path, true); + } + + public Result Format(bool closeOpenEntries) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/LibHac/FsService/Creators/EncryptedFileSystemCreator.cs b/src/LibHac/FsService/Creators/EncryptedFileSystemCreator.cs new file mode 100644 index 00000000..7c6c20f9 --- /dev/null +++ b/src/LibHac/FsService/Creators/EncryptedFileSystemCreator.cs @@ -0,0 +1,34 @@ +using System; +using LibHac.Fs; +using LibHac.FsSystem; + +namespace LibHac.FsService.Creators +{ + public class EncryptedFileSystemCreator : IEncryptedFileSystemCreator + { + private Keyset Keyset { get; } + + public EncryptedFileSystemCreator(Keyset keyset) + { + Keyset = keyset; + } + + public Result Create(out IFileSystem encryptedFileSystem, IFileSystem baseFileSystem, EncryptedFsKeyId keyId, + ReadOnlySpan encryptionSeed) + { + encryptedFileSystem = default; + + if (keyId < EncryptedFsKeyId.Save || keyId > EncryptedFsKeyId.CustomStorage) + { + return ResultFs.InvalidArgument; + } + + // todo: "proper" key generation instead of a lazy hack + Keyset.SetSdSeed(encryptionSeed.ToArray()); + + encryptedFileSystem = new AesXtsFileSystem(baseFileSystem, Keyset.SdCardKeys[(int)keyId], 0x4000); + + return Result.Success; + } + } +} diff --git a/src/LibHac/FsService/Creators/FileSystemCreators.cs b/src/LibHac/FsService/Creators/FileSystemCreators.cs new file mode 100644 index 00000000..c9c048a9 --- /dev/null +++ b/src/LibHac/FsService/Creators/FileSystemCreators.cs @@ -0,0 +1,22 @@ +namespace LibHac.FsService.Creators +{ + public class FileSystemCreators + { + public IRomFileSystemCreator RomFileSystemCreator { get; set; } + public IPartitionFileSystemCreator PartitionFileSystemCreator { get; set; } + public IStorageOnNcaCreator StorageOnNcaCreator { get; set; } + public IFatFileSystemCreator FatFileSystemCreator { get; set; } + public IHostFileSystemCreator HostFileSystemCreator { get; set; } + public ITargetManagerFileSystemCreator TargetManagerFileSystemCreator { get; set; } + public ISubDirectoryFileSystemCreator SubDirectoryFileSystemCreator { get; set; } + public IBuiltInStorageCreator BuiltInStorageCreator { get; set; } + public ISdStorageCreator SdStorageCreator { get; set; } + public ISaveDataFileSystemCreator SaveDataFileSystemCreator { get; set; } + public IGameCardStorageCreator GameCardStorageCreator { get; set; } + public IGameCardFileSystemCreator GameCardFileSystemCreator { get; set; } + public IEncryptedFileSystemCreator EncryptedFileSystemCreator { get; set; } + public IMemoryStorageCreator MemoryStorageCreator { get; set; } + public IBuiltInStorageFileSystemCreator BuiltInStorageFileSystemCreator { get; set; } + public ISdFileSystemCreator SdFileSystemCreator { get; set; } + } +} diff --git a/src/LibHac/FsService/Creators/IBuiltInStorageCreator.cs b/src/LibHac/FsService/Creators/IBuiltInStorageCreator.cs new file mode 100644 index 00000000..e7e6df62 --- /dev/null +++ b/src/LibHac/FsService/Creators/IBuiltInStorageCreator.cs @@ -0,0 +1,9 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IBuiltInStorageCreator + { + Result Create(out IStorage storage, BisPartitionId partitionId); + } +} diff --git a/src/LibHac/FsService/Creators/IBuiltInStorageFileSystemCreator.cs b/src/LibHac/FsService/Creators/IBuiltInStorageFileSystemCreator.cs new file mode 100644 index 00000000..b94ca90a --- /dev/null +++ b/src/LibHac/FsService/Creators/IBuiltInStorageFileSystemCreator.cs @@ -0,0 +1,11 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IBuiltInStorageFileSystemCreator + { + Result Create(out IFileSystem fileSystem, string rootPath, BisPartitionId partitionId); + Result CreateFatFileSystem(out IFileSystem fileSystem, BisPartitionId partitionId); + Result SetBisRootForHost(BisPartitionId partitionId, string rootPath); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/IEncryptedFileSystemCreator.cs b/src/LibHac/FsService/Creators/IEncryptedFileSystemCreator.cs new file mode 100644 index 00000000..f6cfde96 --- /dev/null +++ b/src/LibHac/FsService/Creators/IEncryptedFileSystemCreator.cs @@ -0,0 +1,18 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IEncryptedFileSystemCreator + { + Result Create(out IFileSystem encryptedFileSystem, IFileSystem baseFileSystem, EncryptedFsKeyId keyId, + ReadOnlySpan encryptionSeed); + } + + public enum EncryptedFsKeyId + { + Save = 0, + Content = 1, + CustomStorage = 2 + } +} diff --git a/src/LibHac/FsService/Creators/IFatFileSystemCreator.cs b/src/LibHac/FsService/Creators/IFatFileSystemCreator.cs new file mode 100644 index 00000000..b86fac56 --- /dev/null +++ b/src/LibHac/FsService/Creators/IFatFileSystemCreator.cs @@ -0,0 +1,9 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IFatFileSystemCreator + { + Result Create(out IFileSystem fileSystem, IStorage baseStorage); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/IGameCardFileSystemCreator.cs b/src/LibHac/FsService/Creators/IGameCardFileSystemCreator.cs new file mode 100644 index 00000000..550ce9c6 --- /dev/null +++ b/src/LibHac/FsService/Creators/IGameCardFileSystemCreator.cs @@ -0,0 +1,9 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IGameCardFileSystemCreator + { + Result Create(out IFileSystem fileSystem, GameCardHandle handle, GameCardPartition partitionType); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/IGameCardStorageCreator.cs b/src/LibHac/FsService/Creators/IGameCardStorageCreator.cs new file mode 100644 index 00000000..e5fdfc19 --- /dev/null +++ b/src/LibHac/FsService/Creators/IGameCardStorageCreator.cs @@ -0,0 +1,11 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IGameCardStorageCreator + { + Result CreateNormal(GameCardHandle handle, out IStorage storage); + Result CreateSecure(GameCardHandle handle, out IStorage storage); + Result CreateWritable(GameCardHandle handle, out IStorage storage); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/IHostFileSystemCreator.cs b/src/LibHac/FsService/Creators/IHostFileSystemCreator.cs new file mode 100644 index 00000000..d7c50ff5 --- /dev/null +++ b/src/LibHac/FsService/Creators/IHostFileSystemCreator.cs @@ -0,0 +1,10 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IHostFileSystemCreator + { + Result Create(out IFileSystem fileSystem, bool someBool); + Result Create(out IFileSystem fileSystem, string path, bool openCaseSensitive); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/IMemoryStorageCreator.cs b/src/LibHac/FsService/Creators/IMemoryStorageCreator.cs new file mode 100644 index 00000000..403dbe01 --- /dev/null +++ b/src/LibHac/FsService/Creators/IMemoryStorageCreator.cs @@ -0,0 +1,11 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IMemoryStorageCreator + { + Result Create(out IStorage storage, out Memory buffer, int storageId); + Result RegisterBuffer(int storageId, Memory buffer); + } +} diff --git a/src/LibHac/FsService/Creators/IPartitionFileSystemCreator.cs b/src/LibHac/FsService/Creators/IPartitionFileSystemCreator.cs new file mode 100644 index 00000000..af99e8cd --- /dev/null +++ b/src/LibHac/FsService/Creators/IPartitionFileSystemCreator.cs @@ -0,0 +1,9 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IPartitionFileSystemCreator + { + Result Create(out IFileSystem fileSystem, IStorage pFsStorage); + } +} diff --git a/src/LibHac/FsService/Creators/IRomFileSystemCreator.cs b/src/LibHac/FsService/Creators/IRomFileSystemCreator.cs new file mode 100644 index 00000000..b2ea3957 --- /dev/null +++ b/src/LibHac/FsService/Creators/IRomFileSystemCreator.cs @@ -0,0 +1,9 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface IRomFileSystemCreator + { + Result Create(out IFileSystem fileSystem, IStorage romFsStorage); + } +} diff --git a/src/LibHac/FsService/Creators/ISaveDataFileSystemCreator.cs b/src/LibHac/FsService/Creators/ISaveDataFileSystemCreator.cs new file mode 100644 index 00000000..dd62d5ee --- /dev/null +++ b/src/LibHac/FsService/Creators/ISaveDataFileSystemCreator.cs @@ -0,0 +1,18 @@ +using System; +using LibHac.Common; +using LibHac.Fs; +using LibHac.FsSystem.Save; + +namespace LibHac.FsService.Creators +{ + public interface ISaveDataFileSystemCreator + { + Result CreateFile(out IFile file, IFileSystem sourceFileSystem, ulong saveDataId, OpenMode openMode); + + Result Create(out IFileSystem fileSystem, out ISaveDataExtraDataAccessor extraDataAccessor, + IFileSystem sourceFileSystem, ulong saveDataId, bool allowDirectorySaveData, bool useDeviceUniqueMac, + SaveDataType type, ITimeStampGenerator timeStampGenerator); + + void SetSdCardEncryptionSeed(ReadOnlySpan seed); + } +} diff --git a/src/LibHac/FsService/Creators/ISdFileSystemCreator.cs b/src/LibHac/FsService/Creators/ISdFileSystemCreator.cs new file mode 100644 index 00000000..041f0217 --- /dev/null +++ b/src/LibHac/FsService/Creators/ISdFileSystemCreator.cs @@ -0,0 +1,10 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface ISdFileSystemCreator + { + Result Create(out IFileSystem fileSystem); + Result Format(bool closeOpenEntries); + } +} diff --git a/src/LibHac/FsService/Creators/ISdStorageCreator.cs b/src/LibHac/FsService/Creators/ISdStorageCreator.cs new file mode 100644 index 00000000..691359bb --- /dev/null +++ b/src/LibHac/FsService/Creators/ISdStorageCreator.cs @@ -0,0 +1,9 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface ISdStorageCreator + { + Result Create(out IStorage storage); + } +} diff --git a/src/LibHac/FsService/Creators/IStorageOnNcaCreator.cs b/src/LibHac/FsService/Creators/IStorageOnNcaCreator.cs new file mode 100644 index 00000000..2d6d8fb3 --- /dev/null +++ b/src/LibHac/FsService/Creators/IStorageOnNcaCreator.cs @@ -0,0 +1,13 @@ +using LibHac.Fs; +using LibHac.FsSystem.NcaUtils; + +namespace LibHac.FsService.Creators +{ + public interface IStorageOnNcaCreator + { + Result Create(out IStorage storage, out NcaFsHeader fsHeader, Nca nca, int fsIndex, bool isCodeFs); + Result CreateWithPatch(out IStorage storage, out NcaFsHeader fsHeader, Nca baseNca, Nca patchNca, int fsIndex, bool isCodeFs); + Result OpenNca(out Nca nca, IStorage ncaStorage); + Result VerifyAcidSignature(IFileSystem codeFileSystem, Nca nca); + } +} diff --git a/src/LibHac/FsService/Creators/ISubDirectoryFileSystemCreator.cs b/src/LibHac/FsService/Creators/ISubDirectoryFileSystemCreator.cs new file mode 100644 index 00000000..3bae3cb7 --- /dev/null +++ b/src/LibHac/FsService/Creators/ISubDirectoryFileSystemCreator.cs @@ -0,0 +1,9 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface ISubDirectoryFileSystemCreator + { + Result Create(out IFileSystem subDirFileSystem, IFileSystem baseFileSystem, string path); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/ITargetManagerFileSystemCreator.cs b/src/LibHac/FsService/Creators/ITargetManagerFileSystemCreator.cs new file mode 100644 index 00000000..e0858de4 --- /dev/null +++ b/src/LibHac/FsService/Creators/ITargetManagerFileSystemCreator.cs @@ -0,0 +1,10 @@ +using LibHac.Fs; + +namespace LibHac.FsService.Creators +{ + public interface ITargetManagerFileSystemCreator + { + Result Create(out IFileSystem fileSystem, bool openCaseSensitive); + Result GetCaseSensitivePath(out bool isSuccess, ref string path); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/Creators/SaveDataFileSystemCreator.cs b/src/LibHac/FsService/Creators/SaveDataFileSystemCreator.cs new file mode 100644 index 00000000..9574b70c --- /dev/null +++ b/src/LibHac/FsService/Creators/SaveDataFileSystemCreator.cs @@ -0,0 +1,70 @@ +using System; +using LibHac.Common; +using LibHac.Fs; +using LibHac.FsSystem; +using LibHac.FsSystem.Save; + +namespace LibHac.FsService.Creators +{ + public class SaveDataFileSystemCreator : ISaveDataFileSystemCreator + { + private Keyset Keyset { get; } + + public SaveDataFileSystemCreator(Keyset keyset) + { + Keyset = keyset; + } + + public Result CreateFile(out IFile file, IFileSystem sourceFileSystem, ulong saveDataId, OpenMode openMode) + { + throw new NotImplementedException(); + } + + public Result Create(out IFileSystem fileSystem, out ISaveDataExtraDataAccessor extraDataAccessor, + IFileSystem sourceFileSystem, ulong saveDataId, bool allowDirectorySaveData, bool useDeviceUniqueMac, + SaveDataType type, ITimeStampGenerator timeStampGenerator) + { + fileSystem = default; + extraDataAccessor = default; + + string saveDataPath = $"/{saveDataId:x16}"; + + Result rc = sourceFileSystem.GetEntryType(out DirectoryEntryType entryType, saveDataPath); + if (rc.IsFailure()) + { + return rc == ResultFs.PathNotFound ? ResultFs.TargetNotFound : rc; + } + + switch (entryType) + { + case DirectoryEntryType.Directory: + if (!allowDirectorySaveData) return ResultFs.InvalidSaveDataEntryType.Log(); + + var subDirFs = new SubdirectoryFileSystem(sourceFileSystem, saveDataPath); + fileSystem = new DirectorySaveDataFileSystem(subDirFs); + + // Todo: Dummy ISaveDataExtraDataAccessor + + return Result.Success; + + case DirectoryEntryType.File: + rc = sourceFileSystem.OpenFile(out IFile saveDataFile, saveDataPath, OpenMode.ReadWrite); + if (rc.IsFailure()) return rc; + + var saveDataStorage = new FileStorage(saveDataFile); + fileSystem = new SaveDataFileSystem(Keyset, saveDataStorage, IntegrityCheckLevel.ErrorOnInvalid, true); + + // Todo: ISaveDataExtraDataAccessor + + return Result.Success; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public void SetSdCardEncryptionSeed(ReadOnlySpan seed) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/LibHac/FsService/Creators/SubDirectoryFileSystemCreator.cs b/src/LibHac/FsService/Creators/SubDirectoryFileSystemCreator.cs new file mode 100644 index 00000000..06983e72 --- /dev/null +++ b/src/LibHac/FsService/Creators/SubDirectoryFileSystemCreator.cs @@ -0,0 +1,20 @@ +using LibHac.Fs; +using LibHac.FsSystem; + +namespace LibHac.FsService.Creators +{ + public class SubDirectoryFileSystemCreator : ISubDirectoryFileSystemCreator + { + public Result Create(out IFileSystem subDirFileSystem, IFileSystem baseFileSystem, string path) + { + subDirFileSystem = default; + + Result rc = baseFileSystem.OpenDirectory(out IDirectory _, path, OpenDirectoryMode.Directory); + if (rc.IsFailure()) return rc; + + subDirFileSystem = new SubdirectoryFileSystem(baseFileSystem, path); + + return Result.Success; + } + } +} diff --git a/src/LibHac/FsService/DefaultFsServerObjects.cs b/src/LibHac/FsService/DefaultFsServerObjects.cs new file mode 100644 index 00000000..469757cf --- /dev/null +++ b/src/LibHac/FsService/DefaultFsServerObjects.cs @@ -0,0 +1,37 @@ +using LibHac.Fs; +using LibHac.FsService.Creators; + +namespace LibHac.FsService +{ + public class DefaultFsServerObjects + { + public FileSystemCreators FsCreators { get; set; } + public IDeviceOperator DeviceOperator { get; set; } + public EmulatedGameCard GameCard { get; set; } + + public static DefaultFsServerObjects GetDefaultEmulatedCreators(IFileSystem rootFileSystem, Keyset keyset) + { + var creators = new FileSystemCreators(); + var gameCard = new EmulatedGameCard(keyset); + + var gcStorageCreator = new EmulatedGameCardStorageCreator(gameCard); + + creators.SubDirectoryFileSystemCreator = new SubDirectoryFileSystemCreator(); + creators.SaveDataFileSystemCreator = new SaveDataFileSystemCreator(keyset); + creators.GameCardStorageCreator = gcStorageCreator; + creators.GameCardFileSystemCreator = new EmulatedGameCardFsCreator(gcStorageCreator, gameCard); + creators.EncryptedFileSystemCreator = new EncryptedFileSystemCreator(keyset); + creators.BuiltInStorageFileSystemCreator = new EmulatedBisFileSystemCreator(rootFileSystem); + creators.SdFileSystemCreator = new EmulatedSdFileSystemCreator(rootFileSystem); + + var deviceOperator = new EmulatedDeviceOperator(gameCard); + + return new DefaultFsServerObjects + { + FsCreators = creators, + DeviceOperator = deviceOperator, + GameCard = gameCard + }; + } + } +} diff --git a/src/LibHac/FsService/EmulatedDeviceOperator.cs b/src/LibHac/FsService/EmulatedDeviceOperator.cs new file mode 100644 index 00000000..5e529756 --- /dev/null +++ b/src/LibHac/FsService/EmulatedDeviceOperator.cs @@ -0,0 +1,38 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsService +{ + class EmulatedDeviceOperator : IDeviceOperator + { + private EmulatedGameCard GameCard { get; set; } + + public EmulatedDeviceOperator(EmulatedGameCard gameCard) + { + GameCard = gameCard; + } + + public Result IsSdCardInserted(out bool isInserted) + { + throw new NotImplementedException(); + } + + public Result IsGameCardInserted(out bool isInserted) + { + isInserted = GameCard.IsGameCardInserted(); + return Result.Success; + } + + public Result GetGameCardHandle(out GameCardHandle handle) + { + if (!GameCard.IsGameCardInserted()) + { + handle = default; + return ResultFs.GameCardNotInsertedOnGetHandle.Log(); + } + + handle = GameCard.GetGameCardHandle(); + return Result.Success; + } + } +} diff --git a/src/LibHac/FsService/EmulatedGameCard.cs b/src/LibHac/FsService/EmulatedGameCard.cs new file mode 100644 index 00000000..b54d20c3 --- /dev/null +++ b/src/LibHac/FsService/EmulatedGameCard.cs @@ -0,0 +1,121 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsService +{ + public class EmulatedGameCard + { + private IStorage CardImageStorage { get; set; } + private int Handle { get; set; } + private XciHeader CardHeader { get; set; } + private Xci CardImage { get; set; } + private Keyset Keyset { get; set; } + + public EmulatedGameCard() { } + + public EmulatedGameCard(Keyset keyset) + { + Keyset = keyset; + } + public GameCardHandle GetGameCardHandle() + { + return new GameCardHandle(Handle); + } + + public bool IsGameCardHandleInvalid(GameCardHandle handle) + { + return Handle != handle.Value; + } + + public bool IsGameCardInserted() + { + return CardImageStorage != null; + } + + public void InsertGameCard(IStorage cardImageStorage) + { + RemoveGameCard(); + + CardImageStorage = cardImageStorage; + + CardImage = new Xci(Keyset, cardImageStorage); + CardHeader = CardImage.Header; + } + + public void RemoveGameCard() + { + if (IsGameCardInserted()) + { + CardImageStorage = null; + Handle++; + } + } + + internal Result GetXci(out Xci xci, GameCardHandle handle) + { + xci = default; + + if (IsGameCardHandleInvalid(handle)) return ResultFs.InvalidGameCardHandleOnRead.Log(); + if (!IsGameCardInserted()) return ResultFs.GameCardNotInserted.Log(); + + xci = CardImage; + return Result.Success; + } + public Result Read(GameCardHandle handle, long offset, Span destination) + { + if (IsGameCardHandleInvalid(handle)) return ResultFs.InvalidGameCardHandleOnRead.Log(); + if (!IsGameCardInserted()) return ResultFs.GameCardNotInserted.Log(); + + return CardImageStorage.Read(offset, destination); + } + + public Result GetGameCardImageHash(Span outBuffer) + { + if (outBuffer.Length < 0x20) return ResultFs.InvalidBufferForGameCard.Log(); + if (!IsGameCardInserted()) return ResultFs.GameCardNotInserted.Log(); + + CardHeader.ImageHash.CopyTo(outBuffer.Slice(0, 0x20)); + return Result.Success; + } + + public Result GetGameCardDeviceId(Span outBuffer) + { + if (outBuffer.Length < 0x10) return ResultFs.InvalidBufferForGameCard.Log(); + if (!IsGameCardInserted()) return ResultFs.GameCardNotInserted.Log(); + + // Skip the security mode check + + // Instead of caching the CardKeyArea data, read the value directly + return CardImageStorage.Read(0x7110, outBuffer.Slice(0, 0x10)); + } + + internal Result GetCardInfo(out GameCardInfo cardInfo, GameCardHandle handle) + { + cardInfo = default; + + if (IsGameCardHandleInvalid(handle)) return ResultFs.InvalidGameCardHandleOnGetCardInfo.Log(); + if (!IsGameCardInserted()) return ResultFs.GameCardNotInserted.Log(); + + cardInfo = GetCardInfoImpl(); + return Result.Success; + } + + private GameCardInfo GetCardInfoImpl() + { + var info = new GameCardInfo(); + + CardHeader.RootPartitionHeaderHash.AsSpan().CopyTo(info.RootPartitionHeaderHash); + info.PackageId = CardHeader.PackageId; + info.Size = GameCard.GetGameCardSizeBytes(CardHeader.GameCardSize); + info.RootPartitionOffset = CardHeader.RootPartitionOffset; + info.RootPartitionHeaderSize = CardHeader.RootPartitionHeaderSize; + info.SecureAreaOffset = GameCard.CardPageToOffset(CardHeader.LimAreaPage); + info.SecureAreaSize = info.Size - info.SecureAreaOffset; + info.UpdateVersion = CardHeader.UppVersion; + info.UpdateTitleId = CardHeader.UppId; + info.Attribute = CardHeader.Flags; + + return info; + } + } +} diff --git a/src/LibHac/FsService/ExternalKeySet.cs b/src/LibHac/FsService/ExternalKeySet.cs new file mode 100644 index 00000000..dd6172ec --- /dev/null +++ b/src/LibHac/FsService/ExternalKeySet.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using LibHac.Fs; +using LibHac.Spl; + +namespace LibHac.FsService +{ + public class ExternalKeySet + { + private readonly object _locker = new object(); + + private Dictionary ExternalKeys { get; set; } = new Dictionary(); + + public Result Add(RightsId rightsId, AccessKey key) + { + lock (_locker) + { + if (ExternalKeys.TryGetValue(rightsId, out AccessKey existingKey)) + { + if (key == existingKey) + { + return Result.Success; + } + + return ResultFs.ExternalKeyAlreadyRegistered.Log(); + } + + ExternalKeys.Add(rightsId, key); + } + + return Result.Success; + } + + public Result Get(RightsId rightsId, out AccessKey key) + { + lock (_locker) + { + if (ExternalKeys.TryGetValue(rightsId, out key)) + { + return Result.Success; + } + + return ResultFs.ExternalKeyNotFound.Log(); + } + } + + public bool Contains(RightsId rightsId) + { + lock (_locker) + { + return ExternalKeys.ContainsKey(rightsId); + } + } + + public bool Remove(RightsId rightsId) + { + lock (_locker) + { + return ExternalKeys.Remove(rightsId); + } + } + + public void Clear() + { + lock (_locker) + { + ExternalKeys.Clear(); + } + } + + public List<(RightsId rightsId, AccessKey key)> ToList() + { + lock (_locker) + { + var list = new List<(RightsId rightsId, AccessKey key)>(ExternalKeys.Count); + + foreach (KeyValuePair kvp in ExternalKeys) + { + list.Add((kvp.Key, kvp.Value)); + } + + return list; + } + } + + public void TrimExcess() => TrimExcess(0); + + public void TrimExcess(int capacity) + { + lock (_locker) + { + int newCapacity = Math.Max(capacity, ExternalKeys.Count); +#if NETCOREAPP + ExternalKeys.TrimExcess(newCapacity); +#else + var trimmedDict = new Dictionary(newCapacity); + + foreach (KeyValuePair kvp in ExternalKeys) + { + trimmedDict.Add(kvp.Key, kvp.Value); + } + + ExternalKeys = trimmedDict; +#endif + } + } + } +} diff --git a/src/LibHac/FsService/FileSystemProxy.cs b/src/LibHac/FsService/FileSystemProxy.cs new file mode 100644 index 00000000..9b18e9e0 --- /dev/null +++ b/src/LibHac/FsService/FileSystemProxy.cs @@ -0,0 +1,589 @@ +using System; +using LibHac.Common; +using LibHac.Fs; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Spl; + +namespace LibHac.FsService +{ + public class FileSystemProxy : IFileSystemProxy + { + private FileSystemProxyCore FsProxyCore { get; } + + /// The client instance to be used for internal operations like save indexer access. + // ReSharper disable once UnusedAutoPropertyAccessor.Local + private FileSystemClient FsClient { get; } + + public long CurrentProcess { get; private set; } + + public long SaveDataSize { get; private set; } + public long SaveDataJournalSize { get; private set; } + public FsPath SaveDataRootPath { get; } = default; + public bool AutoCreateSaveData { get; private set; } + + private const ulong SaveIndexerId = 0x8000000000000000; + + internal FileSystemProxy(FileSystemProxyCore fsProxyCore, FileSystemClient fsClient) + { + FsProxyCore = fsProxyCore; + FsClient = fsClient; + + CurrentProcess = -1; + SaveDataSize = 0x2000000; + SaveDataJournalSize = 0x1000000; + AutoCreateSaveData = true; + } + + public Result OpenFileSystemWithId(out IFileSystem fileSystem, ref FsPath path, TitleId titleId, FileSystemType type) + { + throw new NotImplementedException(); + } + + public Result OpenFileSystemWithPatch(out IFileSystem fileSystem, TitleId titleId, FileSystemType type) + { + throw new NotImplementedException(); + } + + public Result SetCurrentProcess(long processId) + { + CurrentProcess = processId; + + return Result.Success; + } + + public Result GetFreeSpaceSizeForSaveData(out long freeSpaceSize, SaveDataSpaceId spaceId) + { + throw new NotImplementedException(); + } + + public Result OpenDataFileSystemByCurrentProcess(out IFileSystem fileSystem) + { + throw new NotImplementedException(); + } + + public Result OpenDataFileSystemByProgramId(out IFileSystem fileSystem, TitleId titleId) + { + throw new NotImplementedException(); + } + + public Result OpenDataStorageByCurrentProcess(out IStorage storage) + { + throw new NotImplementedException(); + } + + public Result OpenDataStorageByProgramId(out IStorage storage, TitleId programId) + { + throw new NotImplementedException(); + } + + public Result OpenDataStorageByDataId(out IStorage storage, TitleId dataId, StorageId storageId) + { + throw new NotImplementedException(); + } + + public Result OpenPatchDataStorageByCurrentProcess(out IStorage storage) + { + throw new NotImplementedException(); + } + + public Result OpenDataFileSystemWithProgramIndex(out IFileSystem fileSystem, byte programIndex) + { + throw new NotImplementedException(); + } + + public Result OpenDataStorageWithProgramIndex(out IStorage storage, byte programIndex) + { + throw new NotImplementedException(); + } + + public Result RegisterSaveDataFileSystemAtomicDeletion(ReadOnlySpan saveDataIds) + { + throw new NotImplementedException(); + } + + public Result DeleteSaveDataFileSystem(ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result DeleteSaveDataFileSystemBySaveDataSpaceId(SaveDataSpaceId spaceId, ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result DeleteSaveDataFileSystemBySaveDataAttribute(SaveDataSpaceId spaceId, ref SaveDataAttribute attribute) + { + throw new NotImplementedException(); + } + + public Result UpdateSaveDataMacForDebug(SaveDataSpaceId spaceId, ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result CreateSaveDataFileSystem(ref SaveDataAttribute attribute, ref SaveDataCreateInfo createInfo, + ref SaveMetaCreateInfo metaCreateInfo) + { + throw new NotImplementedException(); + } + + public Result CreateSaveDataFileSystemWithHashSalt(ref SaveDataAttribute attribute, ref SaveDataCreateInfo createInfo, + ref SaveMetaCreateInfo metaCreateInfo, ref HashSalt hashSalt) + { + throw new NotImplementedException(); + } + + public Result CreateSaveDataFileSystemBySystemSaveDataId(ref SaveDataAttribute attribute, ref SaveDataCreateInfo createInfo) + { + throw new NotImplementedException(); + } + + public Result ExtendSaveDataFileSystem(SaveDataSpaceId spaceId, ulong saveDataId, long dataSize, long journalSize) + { + throw new NotImplementedException(); + } + + private Result OpenSaveDataFileSystemImpl(out IFileSystem fileSystem, out ulong saveDataId, + SaveDataSpaceId spaceId, ref SaveDataAttribute attribute, bool openReadOnly, bool cacheExtraData) + { + bool hasFixedId = attribute.SaveDataId != 0 && attribute.UserId == UserId.Zero; + + if (hasFixedId) + { + saveDataId = attribute.SaveDataId; + } + else + { + throw new NotImplementedException(); + } + + Result saveFsResult = FsProxyCore.OpenSaveDataFileSystem(out fileSystem, spaceId, saveDataId, + SaveDataRootPath.ToString(), openReadOnly, attribute.Type, cacheExtraData); + + if (saveFsResult.IsSuccess()) return Result.Success; + + if (saveFsResult != ResultFs.PathNotFound && saveFsResult != ResultFs.TargetNotFound) return saveFsResult; + + if (saveDataId != SaveIndexerId) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (hasFixedId) + { + // todo: remove save indexer entry + } + } + + return ResultFs.TargetNotFound; + } + + public Result OpenSaveDataFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId, ref SaveDataAttribute attribute) + { + throw new NotImplementedException(); + } + + public Result OpenReadOnlySaveDataFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId, + ref SaveDataAttribute attribute) + { + throw new NotImplementedException(); + } + + public Result OpenSaveDataFileSystemBySystemSaveDataId(out IFileSystem fileSystem, SaveDataSpaceId spaceId, + ref SaveDataAttribute attribute) + { + // Missing permission check, speed emulation storage type wrapper, and FileSystemInterfaceAdapter + fileSystem = default; + + if (!IsSystemSaveDataId(attribute.SaveDataId)) return ResultFs.InvalidArgument.Log(); + + Result rc = OpenSaveDataFileSystemImpl(out IFileSystem saveFs, out _, spaceId, + ref attribute, false, true); + if (rc.IsFailure()) return rc; + + // Missing check if the current title owns the save data or can open it + + fileSystem = saveFs; + + return Result.Success; + } + + public Result ReadSaveDataFileSystemExtraData(Span extraDataBuffer, ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result ReadSaveDataFileSystemExtraDataBySaveDataSpaceId(Span extraDataBuffer, SaveDataSpaceId spaceId, + ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result ReadSaveDataFileSystemExtraDataBySaveDataAttribute(Span extraDataBuffer, SaveDataSpaceId spaceId, + ref SaveDataAttribute attribute) + { + throw new NotImplementedException(); + } + + public Result WriteSaveDataFileSystemExtraData(ulong saveDataId, SaveDataSpaceId spaceId, ReadOnlySpan extraDataBuffer) + { + throw new NotImplementedException(); + } + + public Result WriteSaveDataFileSystemExtraDataBySaveDataAttribute(ref SaveDataAttribute attribute, SaveDataSpaceId spaceId, + ReadOnlySpan extraDataBuffer, ReadOnlySpan maskBuffer) + { + throw new NotImplementedException(); + } + + public Result WriteSaveDataFileSystemExtraDataWithMask(ulong saveDataId, SaveDataSpaceId spaceId, ReadOnlySpan extraDataBuffer, + ReadOnlySpan maskBuffer) + { + throw new NotImplementedException(); + } + + public Result OpenImageDirectoryFileSystem(out IFileSystem fileSystem, ImageDirectoryId dirId) + { + throw new NotImplementedException(); + } + + public Result SetBisRootForHost(BisPartitionId partitionId, ref FsPath path) + { + throw new NotImplementedException(); + } + + public Result OpenBisFileSystem(out IFileSystem fileSystem, ref FsPath rootPath, BisPartitionId partitionId) + { + fileSystem = default; + + // Missing permission check, speed emulation storage type wrapper, and FileSystemInterfaceAdapter + + Result rc = PathTools.Normalize(out U8Span normalizedPath, rootPath); + if (rc.IsFailure()) return rc; + + return FsProxyCore.OpenBisFileSystem(out fileSystem, normalizedPath.ToString(), partitionId); + } + + public Result OpenBisStorage(out IStorage storage, BisPartitionId partitionId) + { + throw new NotImplementedException(); + } + + public Result InvalidateBisCache() + { + throw new NotImplementedException(); + } + + public Result OpenHostFileSystem(out IFileSystem fileSystem, ref FsPath subPath) + { + throw new NotImplementedException(); + } + + public Result OpenSdCardFileSystem(out IFileSystem fileSystem) + { + // Missing permission check, speed emulation storage type wrapper, and FileSystemInterfaceAdapter + + return FsProxyCore.OpenSdCardFileSystem(out fileSystem); + } + + public Result FormatSdCardFileSystem() + { + throw new NotImplementedException(); + } + + public Result FormatSdCardDryRun() + { + throw new NotImplementedException(); + } + + public Result IsExFatSupported(out bool isSupported) + { + throw new NotImplementedException(); + } + + public Result OpenGameCardStorage(out IStorage storage, GameCardHandle handle, GameCardPartitionRaw partitionId) + { + // Missing permission check and StorageInterfaceAdapter + + return FsProxyCore.OpenGameCardStorage(out storage, handle, partitionId); + } + + public Result OpenDeviceOperator(out IDeviceOperator deviceOperator) + { + // Missing permission check + + return FsProxyCore.OpenDeviceOperator(out deviceOperator); + } + + public Result OpenSaveDataInfoReader(out ISaveDataInfoReader infoReader) + { + throw new NotImplementedException(); + } + + public Result OpenSaveDataInfoReaderBySaveDataSpaceId(out ISaveDataInfoReader infoReader, SaveDataSpaceId spaceId) + { + throw new NotImplementedException(); + } + + public Result OpenSaveDataInfoReaderWithFilter(out ISaveDataInfoReader infoReader, SaveDataSpaceId spaceId, + ref SaveDataFilter filter) + { + throw new NotImplementedException(); + } + + public Result FindSaveDataWithFilter(out long count, Span saveDataInfoBuffer, SaveDataSpaceId spaceId, + ref SaveDataFilter filter) + { + throw new NotImplementedException(); + } + + public Result OpenSaveDataInternalStorageFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId, ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result QuerySaveDataInternalStorageTotalSize(out long size, SaveDataSpaceId spaceId, ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result GetSaveDataCommitId(out long commitId, SaveDataSpaceId spaceId, ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result OpenSaveDataInfoReaderOnlyCacheStorage(out ISaveDataInfoReader infoReader) + { + throw new NotImplementedException(); + } + + public Result OpenSaveDataMetaFile(out IFile file, SaveDataSpaceId spaceId, ref SaveDataAttribute attribute, + SaveMetaType type) + { + throw new NotImplementedException(); + } + + public Result DeleteCacheStorage(short index) + { + throw new NotImplementedException(); + } + + public Result GetCacheStorageSize(out long dataSize, out long journalSize, short index) + { + throw new NotImplementedException(); + } + + public Result ListAccessibleSaveDataOwnerId(out int readCount, Span idBuffer, TitleId programId, int startIndex, + int bufferIdCount) + { + throw new NotImplementedException(); + } + + public Result SetSaveDataSize(long saveDataSize, long saveDataJournalSize) + { + if (saveDataSize < 0 || saveDataJournalSize < 0) + { + return ResultFs.InvalidSize; + } + + SaveDataSize = saveDataSize; + SaveDataJournalSize = saveDataJournalSize; + + return Result.Success; + } + public Result SetSaveDataRootPath(ref FsPath path) + { + // Missing permission check + + if (StringUtils.GetLength(path.Str, FsPath.MaxLength + 1) > FsPath.MaxLength) + { + return ResultFs.TooLongPath; + } + + StringUtils.Copy(SaveDataRootPath.Str, path.Str); + + return Result.Success; + } + + public Result OpenContentStorageFileSystem(out IFileSystem fileSystem, ContentStorageId storageId) + { + // Missing permission check, speed emulation storage type wrapper, and FileSystemInterfaceAdapter + + return FsProxyCore.OpenContentStorageFileSystem(out fileSystem, storageId); + } + + public Result OpenCloudBackupWorkStorageFileSystem(out IFileSystem fileSystem, CloudBackupWorkStorageId storageId) + { + throw new NotImplementedException(); + } + + public Result OpenCustomStorageFileSystem(out IFileSystem fileSystem, CustomStorageId storageId) + { + // Missing permission check, speed emulation storage type wrapper, and FileSystemInterfaceAdapter + + return FsProxyCore.OpenCustomStorageFileSystem(out fileSystem, storageId); + } + + public Result OpenGameCardFileSystem(out IFileSystem fileSystem, GameCardHandle handle, + GameCardPartition partitionId) + { + // Missing permission check and FileSystemInterfaceAdapter + + return FsProxyCore.OpenGameCardFileSystem(out fileSystem, handle, partitionId); + } + + public Result QuerySaveDataTotalSize(out long totalSize, long dataSize, long journalSize) + { + throw new NotImplementedException(); + } + + public Result SetCurrentPosixTimeWithTimeDifference(long time, int difference) + { + throw new NotImplementedException(); + } + + public Result GetRightsId(out RightsId rightsId, TitleId programId, StorageId storageId) + { + throw new NotImplementedException(); + } + + public Result GetRightsIdByPath(out RightsId rightsId, ref FsPath path) + { + throw new NotImplementedException(); + } + + public Result GetRightsIdAndKeyGenerationByPath(out RightsId rightsId, out byte keyGeneration, ref FsPath path) + { + throw new NotImplementedException(); + } + + public Result RegisterExternalKey(ref RightsId rightsId, ref AccessKey externalKey) + { + // Missing permission check + + return FsProxyCore.RegisterExternalKey(ref rightsId, ref externalKey); + } + + public Result UnregisterExternalKey(ref RightsId rightsId) + { + // Missing permission check + + return FsProxyCore.UnregisterExternalKey(ref rightsId); + } + + public Result UnregisterAllExternalKey() + { + // Missing permission check + + return FsProxyCore.UnregisterAllExternalKey(); + } + + public Result SetSdCardEncryptionSeed(ReadOnlySpan seed) + { + // todo: use struct instead of byte span + if (seed.Length != 0x10) return ResultFs.InvalidSize; + + // Missing permission check + + Result rc = FsProxyCore.SetSdCardEncryptionSeed(seed); + if (rc.IsFailure()) return rc; + + // todo: Reset save data indexer + + return Result.Success; + } + + public Result VerifySaveDataFileSystemBySaveDataSpaceId(SaveDataSpaceId spaceId, ulong saveDataId, Span readBuffer) + { + throw new NotImplementedException(); + } + + public Result VerifySaveDataFileSystem(ulong saveDataId, Span readBuffer) + { + throw new NotImplementedException(); + } + + public Result CorruptSaveDataFileSystemByOffset(SaveDataSpaceId spaceId, ulong saveDataId, long offset) + { + throw new NotImplementedException(); + } + + public Result CorruptSaveDataFileSystemBySaveDataSpaceId(SaveDataSpaceId spaceId, ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result CorruptSaveDataFileSystem(ulong saveDataId) + { + throw new NotImplementedException(); + } + + public Result CreatePaddingFile(long size) + { + throw new NotImplementedException(); + } + + public Result DeleteAllPaddingFiles() + { + throw new NotImplementedException(); + } + + public Result DisableAutoSaveDataCreation() + { + AutoCreateSaveData = false; + + return Result.Success; + } + + public Result SetGlobalAccessLogMode(GlobalAccessLogMode mode) + { + // Missing permission check + + return FsProxyCore.SetGlobalAccessLogMode(mode); + } + + public Result GetGlobalAccessLogMode(out GlobalAccessLogMode mode) + { + return FsProxyCore.GetGlobalAccessLogMode(out mode); + } + + public Result GetProgramIndexForAccessLog(out int programIndex, out int programCount) + { + throw new NotImplementedException(); + } + + public Result OutputAccessLogToSdCard(U8Span logString) + { + throw new NotImplementedException(); + } + + public Result RegisterUpdatePartition() + { + throw new NotImplementedException(); + } + + public Result OpenRegisteredUpdatePartition(out IFileSystem fileSystem) + { + throw new NotImplementedException(); + } + + public Result OverrideSaveDataTransferTokenSignVerificationKey(ReadOnlySpan key) + { + throw new NotImplementedException(); + } + + public Result SetSdCardAccessibility(bool isAccessible) + { + throw new NotImplementedException(); + } + + public Result IsSdCardAccessible(out bool isAccessible) + { + throw new NotImplementedException(); + } + + private static bool IsSystemSaveDataId(ulong id) + { + return (long)id < 0; + } + } +} diff --git a/src/LibHac/FsService/FileSystemProxyCore.cs b/src/LibHac/FsService/FileSystemProxyCore.cs new file mode 100644 index 00000000..c53b32da --- /dev/null +++ b/src/LibHac/FsService/FileSystemProxyCore.cs @@ -0,0 +1,317 @@ +using System; +using LibHac.Fs; +using LibHac.FsSystem; +using LibHac.FsSystem.Save; +using LibHac.FsService.Creators; +using LibHac.Spl; + +namespace LibHac.FsService +{ + public class FileSystemProxyCore + { + private FileSystemCreators FsCreators { get; } + private ExternalKeySet ExternalKeys { get; } + private IDeviceOperator DeviceOperator { get; } + + private byte[] SdEncryptionSeed { get; } = new byte[0x10]; + + private const string NintendoDirectoryName = "Nintendo"; + private const string ContentDirectoryName = "Contents"; + + private GlobalAccessLogMode LogMode { get; set; } + + public FileSystemProxyCore(FileSystemCreators fsCreators, ExternalKeySet externalKeys, IDeviceOperator deviceOperator) + { + FsCreators = fsCreators; + ExternalKeys = externalKeys ?? new ExternalKeySet(); + DeviceOperator = deviceOperator; + } + + public Result OpenBisFileSystem(out IFileSystem fileSystem, string rootPath, BisPartitionId partitionId) + { + return FsCreators.BuiltInStorageFileSystemCreator.Create(out fileSystem, rootPath, partitionId); + } + + public Result OpenSdCardFileSystem(out IFileSystem fileSystem) + { + return FsCreators.SdFileSystemCreator.Create(out fileSystem); + } + + public Result OpenGameCardStorage(out IStorage storage, GameCardHandle handle, GameCardPartitionRaw partitionId) + { + switch (partitionId) + { + case GameCardPartitionRaw.Normal: + return FsCreators.GameCardStorageCreator.CreateNormal(handle, out storage); + case GameCardPartitionRaw.Secure: + return FsCreators.GameCardStorageCreator.CreateSecure(handle, out storage); + case GameCardPartitionRaw.Writable: + return FsCreators.GameCardStorageCreator.CreateWritable(handle, out storage); + default: + throw new ArgumentOutOfRangeException(nameof(partitionId), partitionId, null); + } + } + + public Result OpenDeviceOperator(out IDeviceOperator deviceOperator) + { + deviceOperator = DeviceOperator; + return Result.Success; + } + + public Result OpenContentStorageFileSystem(out IFileSystem fileSystem, ContentStorageId storageId) + { + fileSystem = default; + + string contentDirPath = default; + IFileSystem baseFileSystem = default; + bool isEncrypted = false; + Result rc; + + switch (storageId) + { + case ContentStorageId.System: + rc = OpenBisFileSystem(out baseFileSystem, string.Empty, BisPartitionId.System); + contentDirPath = $"/{ContentDirectoryName}"; + break; + case ContentStorageId.User: + rc = OpenBisFileSystem(out baseFileSystem, string.Empty, BisPartitionId.User); + contentDirPath = $"/{ContentDirectoryName}"; + break; + case ContentStorageId.SdCard: + rc = OpenSdCardFileSystem(out baseFileSystem); + contentDirPath = $"/{NintendoDirectoryName}/{ContentDirectoryName}"; + isEncrypted = true; + break; + default: + rc = ResultFs.InvalidArgument; + break; + } + + if (rc.IsFailure()) return rc; + + baseFileSystem.EnsureDirectoryExists(contentDirPath); + + rc = FsCreators.SubDirectoryFileSystemCreator.Create(out IFileSystem subDirFileSystem, + baseFileSystem, contentDirPath); + if (rc.IsFailure()) return rc; + + if (!isEncrypted) + { + fileSystem = subDirFileSystem; + return Result.Success; + } + + return FsCreators.EncryptedFileSystemCreator.Create(out fileSystem, subDirFileSystem, + EncryptedFsKeyId.Content, SdEncryptionSeed); + } + + public Result OpenCustomStorageFileSystem(out IFileSystem fileSystem, CustomStorageId storageId) + { + fileSystem = default; + + switch (storageId) + { + case CustomStorageId.SdCard: + { + Result rc = FsCreators.SdFileSystemCreator.Create(out IFileSystem sdFs); + if (rc.IsFailure()) return rc; + + string customStorageDir = CustomStorage.GetCustomStorageDirectoryName(CustomStorageId.SdCard); + string subDirName = $"/{NintendoDirectoryName}/{customStorageDir}"; + + rc = Util.CreateSubFileSystem(out IFileSystem subFs, sdFs, subDirName, true); + if (rc.IsFailure()) return rc; + + rc = FsCreators.EncryptedFileSystemCreator.Create(out IFileSystem encryptedFs, subFs, + EncryptedFsKeyId.CustomStorage, SdEncryptionSeed); + if (rc.IsFailure()) return rc; + + fileSystem = encryptedFs; + return Result.Success; + } + case CustomStorageId.User: + { + Result rc = FsCreators.BuiltInStorageFileSystemCreator.Create(out IFileSystem userFs, string.Empty, + BisPartitionId.User); + if (rc.IsFailure()) return rc; + + string customStorageDir = CustomStorage.GetCustomStorageDirectoryName(CustomStorageId.User); + string subDirName = $"/{customStorageDir}"; + + rc = Util.CreateSubFileSystem(out IFileSystem subFs, userFs, subDirName, true); + if (rc.IsFailure()) return rc; + + fileSystem = subFs; + return Result.Success; + } + default: + return ResultFs.InvalidArgument.Log(); + } + } + + public Result OpenGameCardFileSystem(out IFileSystem fileSystem, GameCardHandle handle, + GameCardPartition partitionId) + { + return FsCreators.GameCardFileSystemCreator.Create(out fileSystem, handle, partitionId); + } + + public Result RegisterExternalKey(ref RightsId rightsId, ref AccessKey externalKey) + { + return ExternalKeys.Add(rightsId, externalKey); + } + + public Result UnregisterExternalKey(ref RightsId rightsId) + { + ExternalKeys.Remove(rightsId); + + return Result.Success; + } + + public Result UnregisterAllExternalKey() + { + ExternalKeys.Clear(); + + return Result.Success; + } + + public Result SetSdCardEncryptionSeed(ReadOnlySpan seed) + { + seed.CopyTo(SdEncryptionSeed); + //FsCreators.SaveDataFileSystemCreator.SetSdCardEncryptionSeed(seed); + + return Result.Success; + } + + public bool AllowDirectorySaveData(SaveDataSpaceId spaceId, string saveDataRootPath) + { + return spaceId == SaveDataSpaceId.User && !string.IsNullOrWhiteSpace(saveDataRootPath); + } + + public Result OpenSaveDataFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId, ulong saveDataId, + string saveDataRootPath, bool openReadOnly, SaveDataType type, bool cacheExtraData) + { + fileSystem = default; + + Result rc = OpenSaveDataDirectory(out IFileSystem saveDirFs, spaceId, saveDataRootPath, true); + if (rc.IsFailure()) return rc; + + bool allowDirectorySaveData = AllowDirectorySaveData(spaceId, saveDataRootPath); + bool useDeviceUniqueMac = Util.UseDeviceUniqueSaveMac(spaceId); + + if (allowDirectorySaveData) + { + try + { + saveDirFs.EnsureDirectoryExists(GetSaveDataIdPath(saveDataId)); + } + catch (HorizonResultException ex) + { + return ex.ResultValue; + } + } + + // Missing save FS cache lookup + + rc = FsCreators.SaveDataFileSystemCreator.Create(out IFileSystem saveFs, out _, saveDirFs, saveDataId, + allowDirectorySaveData, useDeviceUniqueMac, type, null); + + if (rc.IsFailure()) return rc; + + if (cacheExtraData) + { + // Missing extra data caching + } + + fileSystem = openReadOnly ? new ReadOnlyFileSystem(saveFs) : saveFs; + + return Result.Success; + } + + public Result OpenSaveDataDirectory(out IFileSystem fileSystem, SaveDataSpaceId spaceId, string saveDataRootPath, bool openOnHostFs) + { + if (openOnHostFs && AllowDirectorySaveData(spaceId, saveDataRootPath)) + { + Result rc = FsCreators.TargetManagerFileSystemCreator.Create(out IFileSystem hostFs, false); + + if (rc.IsFailure()) + { + fileSystem = default; + return rc; + } + + return Util.CreateSubFileSystem(out fileSystem, hostFs, saveDataRootPath, true); + } + + string dirName = spaceId == SaveDataSpaceId.TemporaryStorage ? "/temp" : "/save"; + + return OpenSaveDataDirectoryImpl(out fileSystem, spaceId, dirName, true); + } + + public Result OpenSaveDataDirectoryImpl(out IFileSystem fileSystem, SaveDataSpaceId spaceId, string saveDirName, bool createIfMissing) + { + fileSystem = default; + Result rc; + + switch (spaceId) + { + case SaveDataSpaceId.System: + rc = OpenBisFileSystem(out IFileSystem sysFs, string.Empty, BisPartitionId.System); + if (rc.IsFailure()) return rc; + + return Util.CreateSubFileSystem(out fileSystem, sysFs, saveDirName, createIfMissing); + + case SaveDataSpaceId.User: + case SaveDataSpaceId.TemporaryStorage: + rc = OpenBisFileSystem(out IFileSystem userFs, string.Empty, BisPartitionId.User); + if (rc.IsFailure()) return rc; + + return Util.CreateSubFileSystem(out fileSystem, userFs, saveDirName, createIfMissing); + + case SaveDataSpaceId.SdSystem: + case SaveDataSpaceId.SdCache: + rc = OpenSdCardFileSystem(out IFileSystem sdFs); + if (rc.IsFailure()) return rc; + + string sdSaveDirPath = $"/{NintendoDirectoryName}{saveDirName}"; + + rc = Util.CreateSubFileSystem(out IFileSystem sdSubFs, sdFs, sdSaveDirPath, createIfMissing); + if (rc.IsFailure()) return rc; + + return FsCreators.EncryptedFileSystemCreator.Create(out fileSystem, sdSubFs, + EncryptedFsKeyId.Save, SdEncryptionSeed); + + case SaveDataSpaceId.ProperSystem: + rc = OpenBisFileSystem(out IFileSystem sysProperFs, string.Empty, BisPartitionId.SystemProperPartition); + if (rc.IsFailure()) return rc; + + return Util.CreateSubFileSystem(out fileSystem, sysProperFs, saveDirName, createIfMissing); + + case SaveDataSpaceId.Safe: + rc = OpenBisFileSystem(out IFileSystem safeFs, string.Empty, BisPartitionId.SafeMode); + if (rc.IsFailure()) return rc; + + return Util.CreateSubFileSystem(out fileSystem, safeFs, saveDirName, createIfMissing); + + default: + return ResultFs.InvalidArgument.Log(); + } + } + + public Result SetGlobalAccessLogMode(GlobalAccessLogMode mode) + { + LogMode = mode; + return Result.Success; + } + + public Result GetGlobalAccessLogMode(out GlobalAccessLogMode mode) + { + mode = LogMode; + return Result.Success; + } + + private string GetSaveDataIdPath(ulong saveDataId) + { + return $"/{saveDataId:x16}"; + } + } +} diff --git a/src/LibHac/FsService/FileSystemServer.cs b/src/LibHac/FsService/FileSystemServer.cs new file mode 100644 index 00000000..c1ae6de2 --- /dev/null +++ b/src/LibHac/FsService/FileSystemServer.cs @@ -0,0 +1,86 @@ +using System; +using LibHac.Fs; +using LibHac.FsService.Creators; + +namespace LibHac.FsService +{ + public class FileSystemServer + { + private FileSystemProxyCore FsProxyCore { get; } + + /// The client instance to be used for internal operations like save indexer access. + private FileSystemClient FsClient { get; } + private ITimeSpanGenerator Timer { get; } + + /// + /// Creates a new . + /// + /// The configuration for the created . + public FileSystemServer(FileSystemServerConfig config) + { + if(config.FsCreators == null) + throw new ArgumentException("FsCreators must not be null"); + + if(config.DeviceOperator == null) + throw new ArgumentException("DeviceOperator must not be null"); + + ExternalKeySet externalKeySet = config.ExternalKeySet ?? new ExternalKeySet(); + ITimeSpanGenerator timer = config.TimeSpanGenerator ?? new StopWatchTimeSpanGenerator(); + + FsProxyCore = new FileSystemProxyCore(config.FsCreators, externalKeySet, config.DeviceOperator); + FsClient = new FileSystemClient(this, timer); + Timer = timer; + } + + /// + /// Creates a new using this 's + /// for the client's access log. + /// + /// The created . + public FileSystemClient CreateFileSystemClient() => CreateFileSystemClient(Timer); + + /// + /// Creates a new . + /// + /// The to use for the created + /// 's access log. + /// The created . + public FileSystemClient CreateFileSystemClient(ITimeSpanGenerator timer) + { + return new FileSystemClient(this, timer); + } + + public IFileSystemProxy CreateFileSystemProxyService() + { + return new FileSystemProxy(FsProxyCore, FsClient); + } + } + + /// + /// Contains the configuration for creating a new . + /// + public class FileSystemServerConfig + { + /// + /// The used for creating filesystems. + /// + public FileSystemCreators FsCreators { get; set; } + + /// + /// An for managing the gamecard and SD card. + /// + public IDeviceOperator DeviceOperator { get; set; } + + /// + /// A keyset containing rights IDs and title keys. + /// If null, an empty set will be created. + /// + public ExternalKeySet ExternalKeySet { get; set; } + + /// + /// Used for generating access log timestamps. + /// If null, a new will be created. + /// + public ITimeSpanGenerator TimeSpanGenerator { get; set; } + } +} diff --git a/src/LibHac/FsService/GameCardHandle.cs b/src/LibHac/FsService/GameCardHandle.cs new file mode 100644 index 00000000..0144fbc2 --- /dev/null +++ b/src/LibHac/FsService/GameCardHandle.cs @@ -0,0 +1,20 @@ +using System; + +namespace LibHac.FsService +{ + public struct GameCardHandle : IEquatable + { + public readonly int Value; + + public GameCardHandle(int value) + { + Value = value; + } + + public override bool Equals(object obj) => obj is GameCardHandle handle && Equals(handle); + public bool Equals(GameCardHandle other) => Value == other.Value; + public override int GetHashCode() => Value.GetHashCode(); + public static bool operator ==(GameCardHandle left, GameCardHandle right) => left.Equals(right); + public static bool operator !=(GameCardHandle left, GameCardHandle right) => !(left == right); + } +} diff --git a/src/LibHac/FsService/GameCardInfo.cs b/src/LibHac/FsService/GameCardInfo.cs new file mode 100644 index 00000000..2047473d --- /dev/null +++ b/src/LibHac/FsService/GameCardInfo.cs @@ -0,0 +1,18 @@ +using LibHac.Fs; + +namespace LibHac.FsService +{ + internal class GameCardInfo + { + public byte[] RootPartitionHeaderHash { get; } = new byte[0x20]; + public ulong PackageId { get; set; } + public long Size { get; set; } + public long RootPartitionOffset { get; set; } + public long RootPartitionHeaderSize { get; set; } + public long SecureAreaOffset { get; set; } + public long SecureAreaSize { get; set; } + public int UpdateVersion { get; set; } + public ulong UpdateTitleId { get; set; } + public GameCardAttribute Attribute { get; set; } + } +} diff --git a/src/LibHac/FsService/IDeviceOperator.cs b/src/LibHac/FsService/IDeviceOperator.cs new file mode 100644 index 00000000..bbb5b218 --- /dev/null +++ b/src/LibHac/FsService/IDeviceOperator.cs @@ -0,0 +1,9 @@ +namespace LibHac.FsService +{ + public interface IDeviceOperator + { + Result IsSdCardInserted(out bool isInserted); + Result IsGameCardInserted(out bool isInserted); + Result GetGameCardHandle(out GameCardHandle handle); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/IFileSystemProxy.cs b/src/LibHac/FsService/IFileSystemProxy.cs new file mode 100644 index 00000000..525b1f33 --- /dev/null +++ b/src/LibHac/FsService/IFileSystemProxy.cs @@ -0,0 +1,103 @@ +using System; +using LibHac.Common; +using LibHac.Fs; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Spl; + +namespace LibHac.FsService +{ + public interface IFileSystemProxy + { + Result SetCurrentProcess(long processId); + Result OpenDataFileSystemByCurrentProcess(out IFileSystem fileSystem); + Result OpenFileSystemWithPatch(out IFileSystem fileSystem, TitleId titleId, FileSystemType type); + Result OpenFileSystemWithId(out IFileSystem fileSystem, ref FsPath path, TitleId titleId, FileSystemType type); + Result OpenDataFileSystemByProgramId(out IFileSystem fileSystem, TitleId titleId); + Result OpenBisFileSystem(out IFileSystem fileSystem, ref FsPath rootPath, BisPartitionId partitionId); + Result OpenBisStorage(out IStorage storage, BisPartitionId partitionId); + Result InvalidateBisCache(); + Result OpenHostFileSystem(out IFileSystem fileSystem, ref FsPath subPath); + Result OpenSdCardFileSystem(out IFileSystem fileSystem); + Result FormatSdCardFileSystem(); + Result DeleteSaveDataFileSystem(ulong saveDataId); + Result CreateSaveDataFileSystem(ref SaveDataAttribute attribute, ref SaveDataCreateInfo createInfo, ref SaveMetaCreateInfo metaCreateInfo); + Result CreateSaveDataFileSystemBySystemSaveDataId(ref SaveDataAttribute attribute, ref SaveDataCreateInfo createInfo); + Result RegisterSaveDataFileSystemAtomicDeletion(ReadOnlySpan saveDataIds); + Result DeleteSaveDataFileSystemBySaveDataSpaceId(SaveDataSpaceId spaceId, ulong saveDataId); + Result FormatSdCardDryRun(); + Result IsExFatSupported(out bool isSupported); + Result DeleteSaveDataFileSystemBySaveDataAttribute(SaveDataSpaceId spaceId, ref SaveDataAttribute attribute); + Result OpenGameCardStorage(out IStorage storage, GameCardHandle handle, GameCardPartitionRaw partitionId); + Result OpenGameCardFileSystem(out IFileSystem fileSystem, GameCardHandle handle, GameCardPartition partitionId); + Result ExtendSaveDataFileSystem(SaveDataSpaceId spaceId, ulong saveDataId, long dataSize, long journalSize); + Result DeleteCacheStorage(short index); + Result GetCacheStorageSize(out long dataSize, out long journalSize, short index); + Result CreateSaveDataFileSystemWithHashSalt(ref SaveDataAttribute attribute, ref SaveDataCreateInfo createInfo, ref SaveMetaCreateInfo metaCreateInfo, ref HashSalt hashSalt); + Result OpenSaveDataFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId, ref SaveDataAttribute attribute); + Result OpenSaveDataFileSystemBySystemSaveDataId(out IFileSystem fileSystem, SaveDataSpaceId spaceId, ref SaveDataAttribute attribute); + Result OpenReadOnlySaveDataFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId, ref SaveDataAttribute attribute); + Result ReadSaveDataFileSystemExtraDataBySaveDataSpaceId(Span extraDataBuffer, SaveDataSpaceId spaceId, ulong saveDataId); + Result ReadSaveDataFileSystemExtraData(Span extraDataBuffer, ulong saveDataId); + Result WriteSaveDataFileSystemExtraData(ulong saveDataId, SaveDataSpaceId spaceId, ReadOnlySpan extraDataBuffer); + Result OpenSaveDataInfoReader(out ISaveDataInfoReader infoReader); + Result OpenSaveDataInfoReaderBySaveDataSpaceId(out ISaveDataInfoReader infoReader, SaveDataSpaceId spaceId); + Result OpenSaveDataInfoReaderOnlyCacheStorage(out ISaveDataInfoReader infoReader); + Result OpenSaveDataInternalStorageFileSystem(out IFileSystem fileSystem, SaveDataSpaceId spaceId, ulong saveDataId); + Result UpdateSaveDataMacForDebug(SaveDataSpaceId spaceId, ulong saveDataId); + Result WriteSaveDataFileSystemExtraDataWithMask(ulong saveDataId, SaveDataSpaceId spaceId, ReadOnlySpan extraDataBuffer, ReadOnlySpan maskBuffer); + Result FindSaveDataWithFilter(out long count, Span saveDataInfoBuffer, SaveDataSpaceId spaceId, ref SaveDataFilter filter); + Result OpenSaveDataInfoReaderWithFilter(out ISaveDataInfoReader infoReader, SaveDataSpaceId spaceId, ref SaveDataFilter filter); + Result ReadSaveDataFileSystemExtraDataBySaveDataAttribute(Span extraDataBuffer, SaveDataSpaceId spaceId, ref SaveDataAttribute attribute); + Result WriteSaveDataFileSystemExtraDataBySaveDataAttribute(ref SaveDataAttribute attribute, SaveDataSpaceId spaceId, ReadOnlySpan extraDataBuffer, ReadOnlySpan maskBuffer); + Result OpenSaveDataMetaFile(out IFile file, SaveDataSpaceId spaceId, ref SaveDataAttribute attribute, SaveMetaType type); + + Result ListAccessibleSaveDataOwnerId(out int readCount, Span idBuffer, TitleId programId, int startIndex, int bufferIdCount); + Result OpenImageDirectoryFileSystem(out IFileSystem fileSystem, ImageDirectoryId dirId); + Result OpenContentStorageFileSystem(out IFileSystem fileSystem, ContentStorageId storageId); + Result OpenCloudBackupWorkStorageFileSystem(out IFileSystem fileSystem, CloudBackupWorkStorageId storageId); + Result OpenCustomStorageFileSystem(out IFileSystem fileSystem, CustomStorageId storageId); + Result OpenDataStorageByCurrentProcess(out IStorage storage); + Result OpenDataStorageByProgramId(out IStorage storage, TitleId programId); + Result OpenDataStorageByDataId(out IStorage storage, TitleId dataId, StorageId storageId); + Result OpenPatchDataStorageByCurrentProcess(out IStorage storage); + Result OpenDataFileSystemWithProgramIndex(out IFileSystem fileSystem, byte programIndex); + Result OpenDataStorageWithProgramIndex(out IStorage storage, byte programIndex); + Result OpenDeviceOperator(out IDeviceOperator deviceOperator); + + Result QuerySaveDataTotalSize(out long totalSize, long dataSize, long journalSize); + Result VerifySaveDataFileSystem(ulong saveDataId, Span readBuffer); + Result CorruptSaveDataFileSystem(ulong saveDataId); + Result CreatePaddingFile(long size); + Result DeleteAllPaddingFiles(); + Result GetRightsId(out RightsId rightsId, TitleId programId, StorageId storageId); + Result RegisterExternalKey(ref RightsId rightsId, ref AccessKey externalKey); + Result UnregisterAllExternalKey(); + Result GetRightsIdByPath(out RightsId rightsId, ref FsPath path); + Result GetRightsIdAndKeyGenerationByPath(out RightsId rightsId, out byte keyGeneration, ref FsPath path); + Result SetCurrentPosixTimeWithTimeDifference(long time, int difference); + Result GetFreeSpaceSizeForSaveData(out long freeSpaceSize, SaveDataSpaceId spaceId); + Result VerifySaveDataFileSystemBySaveDataSpaceId(SaveDataSpaceId spaceId, ulong saveDataId, Span readBuffer); + Result CorruptSaveDataFileSystemBySaveDataSpaceId(SaveDataSpaceId spaceId, ulong saveDataId); + Result QuerySaveDataInternalStorageTotalSize(out long size, SaveDataSpaceId spaceId, ulong saveDataId); + Result GetSaveDataCommitId(out long commitId, SaveDataSpaceId spaceId, ulong saveDataId); + Result UnregisterExternalKey(ref RightsId rightsId); + Result SetSdCardEncryptionSeed(ReadOnlySpan seed); + Result SetSdCardAccessibility(bool isAccessible); + Result IsSdCardAccessible(out bool isAccessible); + + Result SetBisRootForHost(BisPartitionId partitionId, ref FsPath path); + Result SetSaveDataSize(long saveDataSize, long saveDataJournalSize); + Result SetSaveDataRootPath(ref FsPath path); + Result DisableAutoSaveDataCreation(); + Result SetGlobalAccessLogMode(GlobalAccessLogMode mode); + Result GetGlobalAccessLogMode(out GlobalAccessLogMode mode); + Result OutputAccessLogToSdCard(U8Span logString); + Result RegisterUpdatePartition(); + Result OpenRegisteredUpdatePartition(out IFileSystem fileSystem); + + Result GetProgramIndexForAccessLog(out int programIndex, out int programCount); + Result OverrideSaveDataTransferTokenSignVerificationKey(ReadOnlySpan key); + Result CorruptSaveDataFileSystemByOffset(SaveDataSpaceId spaceId, ulong saveDataId, long offset); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/ISaveDataInfoReader.cs b/src/LibHac/FsService/ISaveDataInfoReader.cs new file mode 100644 index 00000000..d0ece142 --- /dev/null +++ b/src/LibHac/FsService/ISaveDataInfoReader.cs @@ -0,0 +1,9 @@ +using System; + +namespace LibHac.FsService +{ + public interface ISaveDataInfoReader + { + Result ReadSaveDataInfo(out long readCount, Span saveDataInfoBuffer); + } +} \ No newline at end of file diff --git a/src/LibHac/FsService/Permissions.cs b/src/LibHac/FsService/Permissions.cs new file mode 100644 index 00000000..2163df8b --- /dev/null +++ b/src/LibHac/FsService/Permissions.cs @@ -0,0 +1,169 @@ +using System; + +namespace LibHac.FsService +{ + /// + /// Permissions that control which filesystems or storages can be mounted or opened. + /// + public enum AccessPermissions + { + MountLogo = 0x0, + MountContentMeta = 0x1, + MountContentControl = 0x2, + MountContentManual = 0x3, + MountContentData = 0x4, + MountApplicationPackage = 0x5, + MountSaveDataStorage = 0x6, + MountContentStorage = 0x7, + MountImageAndVideoStorage = 0x8, + MountCloudBackupWorkStorage = 0x9, + MountCustomStorage = 0xA, + MountBisCalibrationFile = 0xB, + MountBisSafeMode = 0xC, + MountBisUser = 0xD, + MountBisSystem = 0xE, + MountBisSystemProperEncryption = 0xF, + MountBisSystemProperPartition = 0x10, + MountSdCard = 0x11, + MountGameCard = 0x12, + MountDeviceSaveData = 0x13, + MountSystemSaveData = 0x14, + MountOthersSaveData = 0x15, + MountOthersSystemSaveData = 0x16, + OpenBisPartitionBootPartition1Root = 0x17, + OpenBisPartitionBootPartition2Root = 0x18, + OpenBisPartitionUserDataRoot = 0x19, + OpenBisPartitionBootConfigAndPackage2Part1 = 0x1A, + OpenBisPartitionBootConfigAndPackage2Part2 = 0x1B, + OpenBisPartitionBootConfigAndPackage2Part3 = 0x1C, + OpenBisPartitionBootConfigAndPackage2Part4 = 0x1D, + OpenBisPartitionBootConfigAndPackage2Part5 = 0x1E, + OpenBisPartitionBootConfigAndPackage2Part6 = 0x1F, + OpenBisPartitionCalibrationBinary = 0x20, + OpenBisPartitionCalibrationFile = 0x21, + OpenBisPartitionSafeMode = 0x22, + OpenBisPartitionUser = 0x23, + OpenBisPartitionSystem = 0x24, + OpenBisPartitionSystemProperEncryption = 0x25, + OpenBisPartitionSystemProperPartition = 0x26, + OpenSdCardStorage = 0x27, + OpenGameCardStorage = 0x28, + MountSystemDataPrivate = 0x29, + MountHost = 0x2A, + MountRegisteredUpdatePartition = 0x2B, + MountSaveDataInternalStorage = 0x2C, + NotMountCustomStorage = 0x2D + } + + /// + /// Permissions that control which actions can be performed. + /// + public enum ActionPermissions + { + // Todo + } + + public static class PermissionUtils + { + public static ulong GetPermissionMask(AccessPermissions id) + { + switch (id) + { + case AccessPermissions.MountLogo: + return 0x8000000000000801; + case AccessPermissions.MountContentMeta: + return 0x8000000000000801; + case AccessPermissions.MountContentControl: + return 0x8000000000000801; + case AccessPermissions.MountContentManual: + return 0x8000000000000801; + case AccessPermissions.MountContentData: + return 0x8000000000000801; + case AccessPermissions.MountApplicationPackage: + return 0x8000000000000801; + case AccessPermissions.MountSaveDataStorage: + return 0x8000000000000000; + case AccessPermissions.MountContentStorage: + return 0x8000000000000800; + case AccessPermissions.MountImageAndVideoStorage: + return 0x8000000000001000; + case AccessPermissions.MountCloudBackupWorkStorage: + return 0x8000000200000000; + case AccessPermissions.MountCustomStorage: + return 0x8000000000000000; + case AccessPermissions.MountBisCalibrationFile: + return 0x8000000000000084; + case AccessPermissions.MountBisSafeMode: + return 0x8000000000000080; + case AccessPermissions.MountBisUser: + return 0x8000000000008080; + case AccessPermissions.MountBisSystem: + return 0x8000000000008080; + case AccessPermissions.MountBisSystemProperEncryption: + return 0x8000000000000080; + case AccessPermissions.MountBisSystemProperPartition: + return 0x8000000000000080; + case AccessPermissions.MountSdCard: + return 0xC000000000200000; + case AccessPermissions.MountGameCard: + return 0x8000000000000010; + case AccessPermissions.MountDeviceSaveData: + return 0x8000000000040020; + case AccessPermissions.MountSystemSaveData: + return 0x8000000000000028; + case AccessPermissions.MountOthersSaveData: + return 0x8000000000000020; + case AccessPermissions.MountOthersSystemSaveData: + return 0x8000000000000020; + case AccessPermissions.OpenBisPartitionBootPartition1Root: + return 0x8000000000010082; + case AccessPermissions.OpenBisPartitionBootPartition2Root: + return 0x8000000000010080; + case AccessPermissions.OpenBisPartitionUserDataRoot: + return 0x8000000000000080; + case AccessPermissions.OpenBisPartitionBootConfigAndPackage2Part1: + return 0x8000000000010080; + case AccessPermissions.OpenBisPartitionBootConfigAndPackage2Part2: + return 0x8000000000010080; + case AccessPermissions.OpenBisPartitionBootConfigAndPackage2Part3: + return 0x8000000000010080; + case AccessPermissions.OpenBisPartitionBootConfigAndPackage2Part4: + return 0x8000000000010080; + case AccessPermissions.OpenBisPartitionBootConfigAndPackage2Part5: + return 0x8000000000010080; + case AccessPermissions.OpenBisPartitionBootConfigAndPackage2Part6: + return 0x8000000000010080; + case AccessPermissions.OpenBisPartitionCalibrationBinary: + return 0x8000000000000084; + case AccessPermissions.OpenBisPartitionCalibrationFile: + return 0x8000000000000084; + case AccessPermissions.OpenBisPartitionSafeMode: + return 0x8000000000000080; + case AccessPermissions.OpenBisPartitionUser: + return 0x8000000000000080; + case AccessPermissions.OpenBisPartitionSystem: + return 0x8000000000000080; + case AccessPermissions.OpenBisPartitionSystemProperEncryption: + return 0x8000000000000080; + case AccessPermissions.OpenBisPartitionSystemProperPartition: + return 0x8000000000000080; + case AccessPermissions.OpenSdCardStorage: + return 0xC000000000200000; + case AccessPermissions.OpenGameCardStorage: + return 0x8000000000000100; + case AccessPermissions.MountSystemDataPrivate: + return 0x8000000000100008; + case AccessPermissions.MountHost: + return 0xC000000000400000; + case AccessPermissions.MountRegisteredUpdatePartition: + return 0x8000000000010000; + case AccessPermissions.MountSaveDataInternalStorage: + return 0x8000000000000000; + case AccessPermissions.NotMountCustomStorage: + return 0x0; + default: + throw new ArgumentOutOfRangeException(nameof(id), id, null); + } + } + } +} diff --git a/src/LibHac/Fs/SaveIndexerStruct.cs b/src/LibHac/FsService/SaveIndexerStruct.cs similarity index 98% rename from src/LibHac/Fs/SaveIndexerStruct.cs rename to src/LibHac/FsService/SaveIndexerStruct.cs index fedada8f..e78ff3a6 100644 --- a/src/LibHac/Fs/SaveIndexerStruct.cs +++ b/src/LibHac/FsService/SaveIndexerStruct.cs @@ -2,7 +2,7 @@ using System.Buffers.Binary; using LibHac.Kvdb; -namespace LibHac.Fs +namespace LibHac.FsService { public class SaveIndexerStruct : IExportable { diff --git a/src/LibHac/FsService/Util.cs b/src/LibHac/FsService/Util.cs new file mode 100644 index 00000000..d41e462b --- /dev/null +++ b/src/LibHac/FsService/Util.cs @@ -0,0 +1,53 @@ +using LibHac.Fs; +using LibHac.FsSystem; + +namespace LibHac.FsService +{ + public static class Util + { + public static Result CreateSubFileSystem(out IFileSystem subFileSystem, IFileSystem baseFileSystem, string path, + bool createPathIfMissing) + { + subFileSystem = default; + + if (!createPathIfMissing) + { + if (path == null) return ResultFs.NullArgument.Log(); + + Result rc = baseFileSystem.GetEntryType(out DirectoryEntryType entryType, path); + + if (rc.IsFailure() || entryType != DirectoryEntryType.Directory) + { + return ResultFs.PathNotFound.Log(); + } + } + + baseFileSystem.EnsureDirectoryExists(path); + + return CreateSubFileSystemImpl(out subFileSystem, baseFileSystem, path); + } + + public static Result CreateSubFileSystemImpl(out IFileSystem subFileSystem, IFileSystem baseFileSystem, string path) + { + subFileSystem = default; + + if (path == null) return ResultFs.NullArgument.Log(); + + Result rc = baseFileSystem.OpenDirectory(out IDirectory _, path, OpenDirectoryMode.Directory); + if (rc.IsFailure()) return rc; + + subFileSystem = new SubdirectoryFileSystem(baseFileSystem, path); + + return Result.Success; + } + + public static bool UseDeviceUniqueSaveMac(SaveDataSpaceId spaceId) + { + return spaceId == SaveDataSpaceId.System || + spaceId == SaveDataSpaceId.User || + spaceId == SaveDataSpaceId.TemporaryStorage || + spaceId == SaveDataSpaceId.ProperSystem || + spaceId == SaveDataSpaceId.Safe; + } + } +} diff --git a/src/LibHac/Fs/Aes128CtrExStorage.cs b/src/LibHac/FsSystem/Aes128CtrExStorage.cs similarity index 82% rename from src/LibHac/Fs/Aes128CtrExStorage.cs rename to src/LibHac/FsSystem/Aes128CtrExStorage.cs index 9dbd3398..491b7ac2 100644 --- a/src/LibHac/Fs/Aes128CtrExStorage.cs +++ b/src/LibHac/FsSystem/Aes128CtrExStorage.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class Aes128CtrExStorage : Aes128CtrStorage { @@ -30,7 +31,7 @@ namespace LibHac.Fs SubsectionOffsets = SubsectionEntries.Select(x => x.Offset).ToList(); } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { AesSubsectionEntry entry = GetSubsectionEntry(offset); @@ -45,7 +46,9 @@ namespace LibHac.Fs lock (_locker) { UpdateCounterSubsection(entry.Counter); - base.ReadImpl(destination.Slice(outPos, bytesToRead), inPos); + + Result rc = base.ReadImpl(inPos, destination.Slice(outPos, bytesToRead)); + if (rc.IsFailure()) return rc; } outPos += bytesToRead; @@ -57,16 +60,18 @@ namespace LibHac.Fs entry = entry.Next; } } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { - throw new NotImplementedException(); + return ResultFs.UnsupportedOperationInAesCtrExStorageWrite.Log(); } - public override void Flush() + protected override Result FlushImpl() { - throw new NotImplementedException(); + return Result.Success; } private AesSubsectionEntry GetSubsectionEntry(long offset) diff --git a/src/LibHac/Fs/Aes128CtrStorage.cs b/src/LibHac/FsSystem/Aes128CtrStorage.cs similarity index 89% rename from src/LibHac/Fs/Aes128CtrStorage.cs rename to src/LibHac/FsSystem/Aes128CtrStorage.cs index c92467eb..e413bff3 100644 --- a/src/LibHac/Fs/Aes128CtrStorage.cs +++ b/src/LibHac/FsSystem/Aes128CtrStorage.cs @@ -1,8 +1,9 @@ using System; using System.Buffers; using System.Buffers.Binary; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class Aes128CtrStorage : SectorStorage { @@ -58,18 +59,21 @@ namespace LibHac.Fs Counter = _decryptor.Counter; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { - base.ReadImpl(destination, offset); + Result rc = base.ReadImpl(offset, destination); + if (rc.IsFailure()) return rc; lock (_locker) { UpdateCounter(_counterOffset + offset); _decryptor.TransformBlock(destination); } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { byte[] encrypted = ArrayPool.Shared.Rent(source.Length); try @@ -83,12 +87,15 @@ namespace LibHac.Fs _decryptor.TransformBlock(encryptedSpan); } - base.WriteImpl(encryptedSpan, offset); + Result rc = base.WriteImpl(offset, encryptedSpan); + if (rc.IsFailure()) return rc; } finally { ArrayPool.Shared.Return(encrypted); } + + return Result.Success; } private void UpdateCounter(long offset) diff --git a/src/LibHac/Fs/Aes128CtrTransform.cs b/src/LibHac/FsSystem/Aes128CtrTransform.cs similarity index 98% rename from src/LibHac/Fs/Aes128CtrTransform.cs rename to src/LibHac/FsSystem/Aes128CtrTransform.cs index 1a430e82..beaf2d7b 100644 --- a/src/LibHac/Fs/Aes128CtrTransform.cs +++ b/src/LibHac/FsSystem/Aes128CtrTransform.cs @@ -4,7 +4,7 @@ using System.Buffers.Binary; using System.Runtime.InteropServices; using System.Security.Cryptography; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class Aes128CtrTransform { diff --git a/src/LibHac/Fs/Aes128XtsStorage.cs b/src/LibHac/FsSystem/Aes128XtsStorage.cs similarity index 81% rename from src/LibHac/Fs/Aes128XtsStorage.cs rename to src/LibHac/FsSystem/Aes128XtsStorage.cs index e4d99161..b879dc92 100644 --- a/src/LibHac/Fs/Aes128XtsStorage.cs +++ b/src/LibHac/FsSystem/Aes128XtsStorage.cs @@ -1,6 +1,7 @@ using System; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class Aes128XtsStorage : SectorStorage { @@ -37,20 +38,23 @@ namespace LibHac.Fs _key2 = key2.ToArray(); } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { int size = destination.Length; long sectorIndex = offset / SectorSize; if (_decryptor == null) _decryptor = new Aes128XtsTransform(_key1, _key2, true); - base.ReadImpl(_tempBuffer.AsSpan(0, size), offset); + Result rc = base.ReadImpl(offset, _tempBuffer.AsSpan(0, size)); + if (rc.IsFailure()) return rc; _decryptor.TransformBlock(_tempBuffer, 0, size, (ulong)sectorIndex); _tempBuffer.AsSpan(0, size).CopyTo(destination); + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { int size = source.Length; long sectorIndex = offset / SectorSize; @@ -60,12 +64,12 @@ namespace LibHac.Fs source.CopyTo(_tempBuffer); _encryptor.TransformBlock(_tempBuffer, 0, size, (ulong)sectorIndex); - base.WriteImpl(_tempBuffer.AsSpan(0, size), offset); + return base.WriteImpl(offset, _tempBuffer.AsSpan(0, size)); } - public override void Flush() + protected override Result FlushImpl() { - BaseStorage.Flush(); + return BaseStorage.Flush(); } } } diff --git a/src/LibHac/Fs/Aes128XtsTransform.cs b/src/LibHac/FsSystem/Aes128XtsTransform.cs similarity index 99% rename from src/LibHac/Fs/Aes128XtsTransform.cs rename to src/LibHac/FsSystem/Aes128XtsTransform.cs index 53aac4c6..9fc2eb55 100644 --- a/src/LibHac/Fs/Aes128XtsTransform.cs +++ b/src/LibHac/FsSystem/Aes128XtsTransform.cs @@ -29,7 +29,7 @@ using System.Buffers; using System.Runtime.InteropServices; using System.Security.Cryptography; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class Aes128XtsTransform { diff --git a/src/LibHac/FsSystem/AesXtsDirectory.cs b/src/LibHac/FsSystem/AesXtsDirectory.cs new file mode 100644 index 00000000..c262ae6d --- /dev/null +++ b/src/LibHac/FsSystem/AesXtsDirectory.cs @@ -0,0 +1,92 @@ +using System; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class AesXtsDirectory : IDirectory + { + private string Path { get; } + private OpenDirectoryMode Mode { get; } + + private IFileSystem BaseFileSystem { get; } + private IDirectory BaseDirectory { get; } + + public AesXtsDirectory(IFileSystem baseFs, IDirectory baseDir, string path, OpenDirectoryMode mode) + { + BaseFileSystem = baseFs; + BaseDirectory = baseDir; + Mode = mode; + Path = path; + } + + public Result Read(out long entriesRead, Span entryBuffer) + { + Result rc = BaseDirectory.Read(out entriesRead, entryBuffer); + if (rc.IsFailure()) return rc; + + for (int i = 0; i < entriesRead; i++) + { + ref DirectoryEntry entry = ref entryBuffer[i]; + + if (entry.Type == DirectoryEntryType.File) + { + if (Mode.HasFlag(OpenDirectoryMode.NoFileSize)) + { + entry.Size = 0; + } + else + { + string entryName = Util.GetUtf8StringNullTerminated(entry.Name); + entry.Size = GetAesXtsFileSize(PathTools.Combine(Path, entryName)); + } + } + } + + return Result.Success; + } + + public Result GetEntryCount(out long entryCount) + { + return BaseDirectory.GetEntryCount(out entryCount); + } + + /// + /// Reads the size of a NAX0 file from its header. Returns 0 on error. + /// + /// + /// + private long GetAesXtsFileSize(string path) + { + const long magicOffset = 0x20; + const long fileSizeOffset = 0x48; + + // Todo: Remove try/catch when more code uses Result + try + { + Result rc = BaseFileSystem.OpenFile(out IFile file, path, OpenMode.Read); + if (rc.IsFailure()) return 0; + + using (file) + { + uint magic = 0; + long fileSize = 0; + long bytesRead; + + file.Read(out bytesRead, magicOffset, SpanHelpers.AsByteSpan(ref magic), ReadOption.None); + if (bytesRead != sizeof(uint) || magic != AesXtsFileHeader.AesXtsFileMagic) return 0; + + file.Read(out bytesRead, fileSizeOffset, SpanHelpers.AsByteSpan(ref fileSize), ReadOption.None); + if (bytesRead != sizeof(long) || magic != AesXtsFileHeader.AesXtsFileMagic) return 0; + + return fileSize; + } + + } + catch (Exception) + { + return 0; + } + } + } +} diff --git a/src/LibHac/Fs/AesXtsFile.cs b/src/LibHac/FsSystem/AesXtsFile.cs similarity index 52% rename from src/LibHac/Fs/AesXtsFile.cs rename to src/LibHac/FsSystem/AesXtsFile.cs index 32c5b922..e4308efb 100644 --- a/src/LibHac/Fs/AesXtsFile.cs +++ b/src/LibHac/FsSystem/AesXtsFile.cs @@ -1,6 +1,7 @@ using System; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class AesXtsFile : FileBase { @@ -9,6 +10,7 @@ namespace LibHac.Fs private byte[] KekSeed { get; } private byte[] VerificationKey { get; } private int BlockSize { get; } + private OpenMode Mode { get; } private AesXtsFileHeader Header { get; } private IStorage BaseStorage { get; } @@ -26,12 +28,14 @@ namespace LibHac.Fs Header = new AesXtsFileHeader(BaseFile); + baseFile.GetSize(out long fileSize).ThrowIfFailure(); + if (!Header.TryDecryptHeader(Path, KekSeed, VerificationKey)) { ThrowHelper.ThrowResult(ResultFs.AesXtsFileHeaderInvalidKeys, "NAX0 key derivation failed."); } - if (HeaderLength + Util.AlignUp(Header.Size, 0x10) > baseFile.GetSize()) + if (HeaderLength + Util.AlignUp(Header.Size, 0x10) > fileSize) { ThrowHelper.ThrowResult(ResultFs.AesXtsFileTooShort, "NAX0 key derivation failed."); } @@ -49,44 +53,69 @@ namespace LibHac.Fs return key; } - public override int Read(Span destination, long offset, ReadOption options) + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) { - int toRead = ValidateReadParamsAndGetSize(destination, offset); + bytesRead = default; - BaseStorage.Read(destination.Slice(0, toRead), offset); + Result rc = ValidateReadParams(out long toRead, offset, destination.Length, Mode); + if (rc.IsFailure()) return rc; - return toRead; + rc = BaseStorage.Read(offset, destination.Slice(0, (int)toRead)); + if (rc.IsFailure()) return rc; + + bytesRead = toRead; + return Result.Success; } - public override void Write(ReadOnlySpan source, long offset, WriteOption options) + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) { - ValidateWriteParams(source, offset); + Result rc = ValidateWriteParams(offset, source.Length, Mode, out bool isResizeNeeded); + if (rc.IsFailure()) return rc; - BaseStorage.Write(source, offset); + if (isResizeNeeded) + { + rc = SetSizeImpl(offset + source.Length); + if (rc.IsFailure()) return rc; + } + + rc = BaseStorage.Write(offset, source); + if (rc.IsFailure()) return rc; if ((options & WriteOption.Flush) != 0) { - Flush(); + return Flush(); } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { - BaseStorage.Flush(); + return BaseStorage.Flush(); } - public override long GetSize() + protected override Result GetSizeImpl(out long size) { - return Header.Size; + size = Header.Size; + return Result.Success; } - public override void SetSize(long size) + protected override Result SetSizeImpl(long size) { Header.SetSize(size, VerificationKey); - BaseFile.Write(Header.ToBytes(false), 0); + Result rc = BaseFile.Write(0, Header.ToBytes(false)); + if (rc.IsFailure()) return rc; - BaseStorage.SetSize(size); + return BaseStorage.SetSize(size); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + BaseFile?.Dispose(); + } } } } diff --git a/src/LibHac/Fs/AesXtsFileHeader.cs b/src/LibHac/FsSystem/AesXtsFileHeader.cs similarity index 95% rename from src/LibHac/Fs/AesXtsFileHeader.cs rename to src/LibHac/FsSystem/AesXtsFileHeader.cs index 68776041..89233eab 100644 --- a/src/LibHac/Fs/AesXtsFileHeader.cs +++ b/src/LibHac/FsSystem/AesXtsFileHeader.cs @@ -2,12 +2,13 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class AesXtsFileHeader { - private const uint AesXtsFileMagic = 0x3058414E; + internal const uint AesXtsFileMagic = 0x3058414E; public byte[] Signature { get; set; } = new byte[0x20]; public uint Magic { get; } public byte[] EncryptedKey1 { get; } = new byte[0x10]; @@ -21,7 +22,9 @@ namespace LibHac.Fs public AesXtsFileHeader(IFile aesXtsFile) { - if (aesXtsFile.GetSize() < 0x80) + aesXtsFile.GetSize(out long fileSize).ThrowIfFailure(); + + if (fileSize < 0x80) { ThrowHelper.ThrowResult(ResultFs.AesXtsFileHeaderTooShort); } diff --git a/src/LibHac/FsSystem/AesXtsFileSystem.cs b/src/LibHac/FsSystem/AesXtsFileSystem.cs new file mode 100644 index 00000000..45c04399 --- /dev/null +++ b/src/LibHac/FsSystem/AesXtsFileSystem.cs @@ -0,0 +1,278 @@ +using System; +using System.Diagnostics; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class AesXtsFileSystem : FileSystemBase + { + public int BlockSize { get; } + + private IFileSystem BaseFileSystem { get; } + private byte[] KekSource { get; } + private byte[] ValidationKey { get; } + + public AesXtsFileSystem(IFileSystem fs, byte[] kekSource, byte[] validationKey, int blockSize) + { + BaseFileSystem = fs; + KekSource = kekSource; + ValidationKey = validationKey; + BlockSize = blockSize; + } + + public AesXtsFileSystem(IFileSystem fs, byte[] keys, int blockSize) + { + BaseFileSystem = fs; + KekSource = keys.AsSpan(0, 0x10).ToArray(); + ValidationKey = keys.AsSpan(0x10, 0x10).ToArray(); + BlockSize = blockSize; + } + + protected override Result CreateDirectoryImpl(string path) + { + return BaseFileSystem.CreateDirectory(path); + } + + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) + { + return CreateFile(path, size, options, new byte[0x20]); + } + + /// + /// Creates a new using the provided key. + /// + /// The full path of the file to create. + /// The initial size of the created file. + /// Flags to control how the file is created. + /// Should usually be + /// The 256-bit key containing a 128-bit data key followed by a 128-bit tweak key. + public Result CreateFile(string path, long size, CreateFileOptions options, byte[] key) + { + long containerSize = AesXtsFile.HeaderLength + Util.AlignUp(size, 0x10); + + Result rc = BaseFileSystem.CreateFile(path, containerSize, options); + if (rc.IsFailure()) return rc; + + var header = new AesXtsFileHeader(key, size, path, KekSource, ValidationKey); + + rc = BaseFileSystem.OpenFile(out IFile baseFile, path, OpenMode.Write); + if (rc.IsFailure()) return rc; + + using (baseFile) + { + rc = baseFile.Write(0, header.ToBytes(false)); + if (rc.IsFailure()) return rc; + } + + return Result.Success; + } + + protected override Result DeleteDirectoryImpl(string path) + { + return BaseFileSystem.DeleteDirectory(path); + } + + protected override Result DeleteDirectoryRecursivelyImpl(string path) + { + return BaseFileSystem.DeleteDirectoryRecursively(path); + } + + protected override Result CleanDirectoryRecursivelyImpl(string path) + { + return BaseFileSystem.CleanDirectoryRecursively(path); + } + + protected override Result DeleteFileImpl(string path) + { + return BaseFileSystem.DeleteFile(path); + } + + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) + { + directory = default; + path = PathTools.Normalize(path); + + Result rc = BaseFileSystem.OpenDirectory(out IDirectory baseDir, path, mode); + if (rc.IsFailure()) return rc; + + directory = new AesXtsDirectory(BaseFileSystem, baseDir, path, mode); + return Result.Success; + } + + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) + { + file = default; + path = PathTools.Normalize(path); + + Result rc = BaseFileSystem.OpenFile(out IFile baseFile, path, mode | OpenMode.Read); + if (rc.IsFailure()) return rc; + + var xtsFile = new AesXtsFile(mode, baseFile, path, KekSource, ValidationKey, BlockSize); + + file = xtsFile; + return Result.Success; + } + + protected override Result RenameDirectoryImpl(string oldPath, string newPath) + { + oldPath = PathTools.Normalize(oldPath); + newPath = PathTools.Normalize(newPath); + + // todo: Return proper result codes + + // Official code procedure: + // Make sure all file headers can be decrypted + // Rename directory to the new path + // Reencrypt file headers with new path + // If no errors, return + // Reencrypt any modified file headers with the old path + // Rename directory to the old path + + Result rc = BaseFileSystem.RenameDirectory(oldPath, newPath); + if (rc.IsFailure()) return rc; + + try + { + RenameDirectoryImpl(oldPath, newPath, false); + } + catch (Exception) + { + RenameDirectoryImpl(oldPath, newPath, true); + BaseFileSystem.RenameDirectory(oldPath, newPath); + + throw; + } + + return Result.Success; + } + + private void RenameDirectoryImpl(string srcDir, string dstDir, bool doRollback) + { + foreach (DirectoryEntryEx entry in this.EnumerateEntries(srcDir, "*")) + { + string subSrcPath = $"{srcDir}/{entry.Name}"; + string subDstPath = $"{dstDir}/{entry.Name}"; + + if (entry.Type == DirectoryEntryType.Directory) + { + RenameDirectoryImpl(subSrcPath, subDstPath, doRollback); + } + + if (entry.Type == DirectoryEntryType.File) + { + if (doRollback) + { + if (TryReadXtsHeader(subDstPath, subDstPath, out AesXtsFileHeader header)) + { + WriteXtsHeader(header, subDstPath, subSrcPath); + } + } + else + { + AesXtsFileHeader header = ReadXtsHeader(subDstPath, subSrcPath); + WriteXtsHeader(header, subDstPath, subDstPath); + } + } + } + } + + protected override Result RenameFileImpl(string oldPath, string newPath) + { + oldPath = PathTools.Normalize(oldPath); + newPath = PathTools.Normalize(newPath); + + // todo: Return proper result codes + + AesXtsFileHeader header = ReadXtsHeader(oldPath, oldPath); + + BaseFileSystem.RenameFile(oldPath, newPath); + + try + { + WriteXtsHeader(header, newPath, newPath); + } + catch (Exception) + { + BaseFileSystem.RenameFile(newPath, oldPath); + WriteXtsHeader(header, oldPath, oldPath); + + throw; + } + + return Result.Success; + } + + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) + { + return BaseFileSystem.GetEntryType(out entryType, path); + } + + protected override Result GetFileTimeStampRawImpl(out FileTimeStampRaw timeStamp, string path) + { + return BaseFileSystem.GetFileTimeStampRaw(out timeStamp, path); + } + + protected override Result GetFreeSpaceSizeImpl(out long freeSpace, string path) + { + return BaseFileSystem.GetFreeSpaceSize(out freeSpace, path); + } + + protected override Result GetTotalSpaceSizeImpl(out long totalSpace, string path) + { + return BaseFileSystem.GetTotalSpaceSize(out totalSpace, path); + } + + protected override Result CommitImpl() + { + return BaseFileSystem.Commit(); + } + + protected override Result QueryEntryImpl(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, string path) + { + return BaseFileSystem.QueryEntry(outBuffer, inBuffer, queryId, path); + } + + private AesXtsFileHeader ReadXtsHeader(string filePath, string keyPath) + { + if (!TryReadXtsHeader(filePath, keyPath, out AesXtsFileHeader header)) + { + ThrowHelper.ThrowResult(ResultFs.AesXtsFileHeaderInvalidKeysInRenameFile, "Could not decrypt AES-XTS keys"); + } + + return header; + } + + private bool TryReadXtsHeader(string filePath, string keyPath, out AesXtsFileHeader header) + { + Debug.Assert(PathTools.IsNormalized(filePath.AsSpan())); + Debug.Assert(PathTools.IsNormalized(keyPath.AsSpan())); + + header = null; + + Result rc = BaseFileSystem.OpenFile(out IFile file, filePath, OpenMode.Read); + if (rc.IsFailure()) return false; + + using (file) + { + header = new AesXtsFileHeader(file); + + return header.TryDecryptHeader(keyPath, KekSource, ValidationKey); + } + } + + private void WriteXtsHeader(AesXtsFileHeader header, string filePath, string keyPath) + { + Debug.Assert(PathTools.IsNormalized(filePath.AsSpan())); + Debug.Assert(PathTools.IsNormalized(keyPath.AsSpan())); + + header.EncryptHeader(keyPath, KekSource, ValidationKey); + + BaseFileSystem.OpenFile(out IFile file, filePath, OpenMode.ReadWrite); + + using (file) + { + file.Write(0, header.ToBytes(false), WriteOption.Flush).ThrowIfFailure(); + } + } + } +} diff --git a/src/LibHac/Fs/BucketTree.cs b/src/LibHac/FsSystem/BucketTree.cs similarity index 98% rename from src/LibHac/Fs/BucketTree.cs rename to src/LibHac/FsSystem/BucketTree.cs index 0c7cdb42..85f6608e 100644 --- a/src/LibHac/Fs/BucketTree.cs +++ b/src/LibHac/FsSystem/BucketTree.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class BucketTree where T : BucketTreeEntry, new() { diff --git a/src/LibHac/Fs/CachedStorage.cs b/src/LibHac/FsSystem/CachedStorage.cs similarity index 71% rename from src/LibHac/Fs/CachedStorage.cs rename to src/LibHac/FsSystem/CachedStorage.cs index a6e874f3..d3303aed 100644 --- a/src/LibHac/Fs/CachedStorage.cs +++ b/src/LibHac/FsSystem/CachedStorage.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class CachedStorage : StorageBase { private IStorage BaseStorage { get; } private int BlockSize { get; } - private long _length; + private long Length { get; set; } + private bool LeaveOpen { get; } private LinkedList Blocks { get; } = new LinkedList(); private Dictionary> BlockDict { get; } = new Dictionary>(); @@ -16,9 +18,10 @@ namespace LibHac.Fs { BaseStorage = baseStorage; BlockSize = blockSize; - _length = BaseStorage.GetSize(); + LeaveOpen = leaveOpen; - if (!leaveOpen) ToDispose.Add(BaseStorage); + BaseStorage.GetSize(out long baseSize).ThrowIfFailure(); + Length = baseSize; for (int i = 0; i < cacheSize; i++) { @@ -30,12 +33,15 @@ namespace LibHac.Fs public CachedStorage(SectorStorage baseStorage, int cacheSize, bool leaveOpen) : this(baseStorage, baseStorage.SectorSize, cacheSize, leaveOpen) { } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { long remaining = destination.Length; long inOffset = offset; int outOffset = 0; + if (!IsRangeValid(offset, destination.Length, Length)) + return ResultFs.ValueOutOfRange.Log(); + lock (Blocks) { while (remaining > 0) @@ -53,14 +59,19 @@ namespace LibHac.Fs remaining -= bytesToRead; } } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { long remaining = source.Length; long inOffset = offset; int outOffset = 0; + if (!IsRangeValid(offset, source.Length, Length)) + return ResultFs.ValueOutOfRange.Log(); + lock (Blocks) { while (remaining > 0) @@ -80,9 +91,11 @@ namespace LibHac.Fs remaining -= bytesToWrite; } } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { lock (Blocks) { @@ -92,16 +105,34 @@ namespace LibHac.Fs } } - BaseStorage.Flush(); + return BaseStorage.Flush(); } - public override long GetSize() => _length; - - public override void SetSize(long size) + protected override Result GetSizeImpl(out long size) { - BaseStorage.SetSize(size); + size = Length; + return Result.Success; + } - _length = BaseStorage.GetSize(); + protected override Result SetSizeImpl(long size) + { + Result rc = BaseStorage.SetSize(size); + if (rc.IsFailure()) return rc; + + rc = BaseStorage.GetSize(out long newSize); + if (rc.IsFailure()) return rc; + + Length = newSize; + + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (!LeaveOpen) + { + BaseStorage?.Dispose(); + } } private CacheBlock GetBlock(long blockIndex) @@ -142,12 +173,12 @@ namespace LibHac.Fs long offset = index * BlockSize; int length = BlockSize; - if (_length != -1) + if (Length != -1) { - length = (int)Math.Min(_length - offset, length); + length = (int)Math.Min(Length - offset, length); } - BaseStorage.Read(block.Buffer.AsSpan(0, length), offset); + BaseStorage.Read(offset, block.Buffer.AsSpan(0, length)).ThrowIfFailure(); block.Length = length; block.Index = index; block.Dirty = false; @@ -158,7 +189,7 @@ namespace LibHac.Fs if (!block.Dirty) return; long offset = block.Index * BlockSize; - BaseStorage.Write(block.Buffer.AsSpan(0, block.Length), offset); + BaseStorage.Write(offset, block.Buffer.AsSpan(0, block.Length)).ThrowIfFailure(); block.Dirty = false; } diff --git a/src/LibHac/FsSystem/ConcatenationDirectory.cs b/src/LibHac/FsSystem/ConcatenationDirectory.cs new file mode 100644 index 00000000..13b60cc7 --- /dev/null +++ b/src/LibHac/FsSystem/ConcatenationDirectory.cs @@ -0,0 +1,119 @@ +using System; +using LibHac.Common; +using LibHac.Fs; +#if CROSS_PLATFORM +using System.Runtime.InteropServices; +#endif + +namespace LibHac.FsSystem +{ + public class ConcatenationDirectory : IDirectory + { + private string Path { get; } + private OpenDirectoryMode Mode { get; } + + private ConcatenationFileSystem ParentFileSystem { get; } + private IFileSystem BaseFileSystem { get; } + private IDirectory ParentDirectory { get; } + + public ConcatenationDirectory(ConcatenationFileSystem fs, IFileSystem baseFs, IDirectory parentDirectory, OpenDirectoryMode mode, string path) + { + ParentFileSystem = fs; + BaseFileSystem = baseFs; + ParentDirectory = parentDirectory; + Mode = mode; + Path = path; + } + + public Result Read(out long entriesRead, Span entryBuffer) + { + entriesRead = 0; + var entry = new DirectoryEntry(); + Span entrySpan = SpanHelpers.AsSpan(ref entry); + + int i; + for (i = 0; i < entryBuffer.Length; i++) + { + Result rc = ParentDirectory.Read(out long baseEntriesRead, entrySpan); + if (rc.IsFailure()) return rc; + + if (baseEntriesRead == 0) break; + + // Check if the current open mode says we should return the entry + bool isConcatFile = IsConcatenationFile(entry); + if (!CanReturnEntry(entry, isConcatFile)) continue; + + if (isConcatFile) + { + entry.Type = DirectoryEntryType.File; + + if (!Mode.HasFlag(OpenDirectoryMode.NoFileSize)) + { + string entryName = Util.GetUtf8StringNullTerminated(entry.Name); + string entryFullPath = PathTools.Combine(Path, entryName); + + entry.Size = ParentFileSystem.GetConcatenationFileSize(entryFullPath); + } + } + + entry.Attributes = NxFileAttributes.None; + + entryBuffer[i] = entry; + } + + entriesRead = i; + return Result.Success; + } + + public Result GetEntryCount(out long entryCount) + { + entryCount = 0; + long count = 0; + + Result rc = BaseFileSystem.OpenDirectory(out IDirectory _, Path, + OpenDirectoryMode.All | OpenDirectoryMode.NoFileSize); + if (rc.IsFailure()) return rc; + + var entry = new DirectoryEntry(); + Span entrySpan = SpanHelpers.AsSpan(ref entry); + + while (true) + { + rc = ParentDirectory.Read(out long baseEntriesRead, entrySpan); + if (rc.IsFailure()) return rc; + + if (baseEntriesRead == 0) break; + + if (CanReturnEntry(entry, IsConcatenationFile(entry))) count++; + } + + entryCount = count; + return Result.Success; + } + + private bool CanReturnEntry(DirectoryEntry entry, bool isConcatFile) + { + return Mode.HasFlag(OpenDirectoryMode.File) && (entry.Type == DirectoryEntryType.File || isConcatFile) || + Mode.HasFlag(OpenDirectoryMode.Directory) && entry.Type == DirectoryEntryType.Directory && !isConcatFile; + } + + private bool IsConcatenationFile(DirectoryEntry entry) + { +#if CROSS_PLATFORM + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return ConcatenationFileSystem.HasConcatenationFileAttribute(entry.Attributes); + } + else + { + string name = Util.GetUtf8StringNullTerminated(entry.Name); + string fullPath = PathTools.Combine(Path, name); + + return ParentFileSystem.IsConcatenationFile(fullPath); + } +#else + return ConcatenationFileSystem.HasConcatenationFileAttribute(entry.Attributes); +#endif + } + } +} diff --git a/src/LibHac/Fs/ConcatenationFile.cs b/src/LibHac/FsSystem/ConcatenationFile.cs similarity index 57% rename from src/LibHac/Fs/ConcatenationFile.cs rename to src/LibHac/FsSystem/ConcatenationFile.cs index ad738465..9165a36e 100644 --- a/src/LibHac/Fs/ConcatenationFile.cs +++ b/src/LibHac/FsSystem/ConcatenationFile.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class ConcatenationFile : FileBase { @@ -11,6 +12,7 @@ namespace LibHac.Fs private string FilePath { get; } private List Sources { get; } private long SubFileSize { get; } + private OpenMode Mode { get; } internal ConcatenationFile(IFileSystem baseFileSystem, string path, IEnumerable sources, long subFileSize, OpenMode mode) { @@ -22,20 +24,26 @@ namespace LibHac.Fs for (int i = 0; i < Sources.Count - 1; i++) { - if (Sources[i].GetSize() != SubFileSize) + Sources[i].GetSize(out long actualSubFileSize).ThrowIfFailure(); + + if (actualSubFileSize != SubFileSize) { throw new ArgumentException($"Source file must have size {subFileSize}"); } } - - ToDispose.AddRange(Sources); } - public override int Read(Span destination, long offset, ReadOption options) + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) { + bytesRead = default; + long inPos = offset; int outPos = 0; - int remaining = ValidateReadParamsAndGetSize(destination, offset); + + Result rc = ValidateReadParams(out long remaining, offset, destination.Length, Mode); + if (rc.IsFailure()) return rc; + + GetSize(out long fileSize).ThrowIfFailure(); while (remaining > 0) { @@ -43,74 +51,92 @@ namespace LibHac.Fs IFile file = Sources[fileIndex]; long fileOffset = offset - fileIndex * SubFileSize; - long fileEndOffset = Math.Min((fileIndex + 1) * SubFileSize, GetSize()); + long fileEndOffset = Math.Min((fileIndex + 1) * SubFileSize, fileSize); int bytesToRead = (int)Math.Min(fileEndOffset - inPos, remaining); - int bytesRead = file.Read(destination.Slice(outPos, bytesToRead), fileOffset, options); - outPos += bytesRead; - inPos += bytesRead; - remaining -= bytesRead; + rc = file.Read(out long subFileBytesRead, fileOffset, destination.Slice(outPos, bytesToRead), options); + if (rc.IsFailure()) return rc; + + outPos += (int)subFileBytesRead; + inPos += subFileBytesRead; + remaining -= subFileBytesRead; if (bytesRead < bytesToRead) break; } - return outPos; + bytesRead = outPos; + + return Result.Success; } - public override void Write(ReadOnlySpan source, long offset, WriteOption options) + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) { - ValidateWriteParams(source, offset); + Result rc = ValidateWriteParams(offset, source.Length, Mode, out _); + if (rc.IsFailure()) return rc; int inPos = 0; long outPos = offset; int remaining = source.Length; + GetSize(out long fileSize).ThrowIfFailure(); + while (remaining > 0) { int fileIndex = GetSubFileIndexFromOffset(outPos); IFile file = Sources[fileIndex]; long fileOffset = outPos - fileIndex * SubFileSize; - long fileEndOffset = Math.Min((fileIndex + 1) * SubFileSize, GetSize()); + long fileEndOffset = Math.Min((fileIndex + 1) * SubFileSize, fileSize); int bytesToWrite = (int)Math.Min(fileEndOffset - outPos, remaining); - file.Write(source.Slice(inPos, bytesToWrite), fileOffset, options); + + rc = file.Write(fileOffset, source.Slice(inPos, bytesToWrite), options); + if (rc.IsFailure()) return rc; outPos += bytesToWrite; inPos += bytesToWrite; remaining -= bytesToWrite; } - if ((options & WriteOption.Flush) != 0) + if (options.HasFlag(WriteOption.Flush)) { - Flush(); + return Flush(); } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { foreach (IFile file in Sources) { - file.Flush(); + Result rc = file.Flush(); + if (rc.IsFailure()) return rc; } + + return Result.Success; } - public override long GetSize() + protected override Result GetSizeImpl(out long size) { - long size = 0; + size = default; foreach (IFile file in Sources) { - size += file.GetSize(); + Result rc = file.GetSize(out long subFileSize); + if (rc.IsFailure()) return rc; + + size += subFileSize; } - return size; + return Result.Success; } - public override void SetSize(long size) + protected override Result SetSizeImpl(long size) { - long currentSize = GetSize(); + Result rc = GetSize(out long currentSize); + if (rc.IsFailure()) return rc; - if (currentSize == size) return; + if (currentSize == size) return Result.Success; int currentSubFileCount = QuerySubFileCount(currentSize, SubFileSize); int newSubFileCount = QuerySubFileCount(size, SubFileSize); @@ -120,15 +146,21 @@ namespace LibHac.Fs IFile currentLastSubFile = Sources[currentSubFileCount - 1]; long newSubFileSize = QuerySubFileSize(currentSubFileCount - 1, size, SubFileSize); - currentLastSubFile.SetSize(newSubFileSize); + rc = currentLastSubFile.SetSize(newSubFileSize); + if (rc.IsFailure()) return rc; for (int i = currentSubFileCount; i < newSubFileCount; i++) { string newSubFilePath = ConcatenationFileSystem.GetSubFilePath(FilePath, i); newSubFileSize = QuerySubFileSize(i, size, SubFileSize); - BaseFileSystem.CreateFile(newSubFilePath, newSubFileSize, CreateFileOptions.None); - Sources.Add(BaseFileSystem.OpenFile(newSubFilePath, Mode)); + rc = BaseFileSystem.CreateFile(newSubFilePath, newSubFileSize, CreateFileOptions.None); + if (rc.IsFailure()) return rc; + + rc = BaseFileSystem.OpenFile(out IFile newSubFile, newSubFilePath, Mode); + if (rc.IsFailure()) return rc; + + Sources.Add(newSubFile); } } else @@ -139,11 +171,30 @@ namespace LibHac.Fs Sources.RemoveAt(i); string subFilePath = ConcatenationFileSystem.GetSubFilePath(FilePath, i); - BaseFileSystem.DeleteFile(subFilePath); + + rc = BaseFileSystem.DeleteFile(subFilePath); + if (rc.IsFailure()) return rc; } long newLastFileSize = QuerySubFileSize(newSubFileCount - 1, size, SubFileSize); - Sources[newSubFileCount - 1].SetSize(newLastFileSize); + + rc = Sources[newSubFileCount - 1].SetSize(newLastFileSize); + if (rc.IsFailure()) return rc; + } + + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + foreach (IFile file in Sources) + { + file?.Dispose(); + } + + Sources.Clear(); } } diff --git a/src/LibHac/FsSystem/ConcatenationFileSystem.cs b/src/LibHac/FsSystem/ConcatenationFileSystem.cs new file mode 100644 index 00000000..f90045ac --- /dev/null +++ b/src/LibHac/FsSystem/ConcatenationFileSystem.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections.Generic; +using LibHac.Fs; +#if CROSS_PLATFORM +using System.Runtime.InteropServices; +#endif + +namespace LibHac.FsSystem +{ + /// + /// An that stores large files as smaller, separate sub-files. + /// + /// + /// This filesystem is mainly used to allow storing large files on filesystems that have low + /// limits on file size such as FAT filesystems. The underlying base filesystem must have + /// support for the "Archive" file attribute found in FAT or NTFS filesystems. + /// + /// A may contain both standard files or Concatenation files. + /// If a directory has the archive attribute set, its contents will be concatenated and treated + /// as a single file. These sub-files must follow the naming scheme "00", "01", "02", ... + /// Each sub-file except the final one must have the size that was specified + /// at the creation of the . + /// + public class ConcatenationFileSystem : FileSystemBase + { + private const long DefaultSubFileSize = 0xFFFF0000; // Hard-coded value used by FS + private IAttributeFileSystem BaseFileSystem { get; } + private long SubFileSize { get; } + + /// + /// Initializes a new . + /// + /// The base for the + /// new . + public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem) : this(baseFileSystem, DefaultSubFileSize) { } + + /// + /// Initializes a new . + /// + /// The base for the + /// new . + /// The size of each sub-file. Once a file exceeds this size, a new sub-file will be created + public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem, long subFileSize) + { + BaseFileSystem = baseFileSystem; + SubFileSize = subFileSize; + } + + // .NET Core on platforms other than Windows doesn't support getting the + // archive flag in FAT file systems. Try to work around that for now for reading, + // but writing still won't work properly on those platforms + internal bool IsConcatenationFile(string path) + { +#if CROSS_PLATFORM + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Result rc = BaseFileSystem.GetFileAttributes(path, out NxFileAttributes attributes); + if (rc.IsFailure()) return false; + + return HasConcatenationFileAttribute(attributes); + } + else + { + return IsConcatenationFileHeuristic(path); + } +#else + Result rc = BaseFileSystem.GetFileAttributes(path, out NxFileAttributes attributes); + if (rc.IsFailure()) return false; + + return HasConcatenationFileAttribute(attributes); +#endif + } + +#if CROSS_PLATFORM + private bool IsConcatenationFileHeuristic(string path) + { + // Check if the path is a directory + Result getTypeResult = BaseFileSystem.GetEntryType(out DirectoryEntryType pathType, path); + if (getTypeResult.IsFailure() || pathType != DirectoryEntryType.Directory) return false; + + // Check if the directory contains at least one subfile + getTypeResult = BaseFileSystem.GetEntryType(out DirectoryEntryType subFileType, PathTools.Combine(path, "00")); + if (getTypeResult.IsFailure() || subFileType != DirectoryEntryType.File) return false; + + // Make sure the directory contains no subdirectories + Result rc = BaseFileSystem.OpenDirectory(out IDirectory dir, path, OpenDirectoryMode.Directory); + if (rc.IsFailure()) return false; + + rc = dir.GetEntryCount(out long subDirCount); + if (rc.IsFailure() || subDirCount > 0) return false; + + // Should be enough checks to avoid most false positives. Maybe + return true; + } +#endif + + internal static bool HasConcatenationFileAttribute(NxFileAttributes attributes) + { + return (attributes & NxFileAttributes.Directory) != 0 && (attributes & NxFileAttributes.Archive) != 0; + } + + private Result SetConcatenationFileAttribute(string path) + { + return BaseFileSystem.SetFileAttributes(path, NxFileAttributes.Archive); + } + + protected override Result CreateDirectoryImpl(string path) + { + path = PathTools.Normalize(path); + string parent = PathTools.GetParentDirectory(path); + + if (IsConcatenationFile(parent)) + { + // Cannot create a directory inside of a concatenation file + return ResultFs.PathNotFound.Log(); + } + + return BaseFileSystem.CreateDirectory(path); + } + + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) + { + path = PathTools.Normalize(path); + + CreateFileOptions newOptions = options & ~CreateFileOptions.CreateConcatenationFile; + + if (!options.HasFlag(CreateFileOptions.CreateConcatenationFile)) + { + return BaseFileSystem.CreateFile(path, size, newOptions); + } + + // A concatenation file directory can't contain normal files + string parentDir = PathTools.GetParentDirectory(path); + + if (IsConcatenationFile(parentDir)) + { + // Cannot create a file inside of a concatenation file + return ResultFs.PathNotFound.Log(); + } + + Result rc = BaseFileSystem.CreateDirectory(path, NxFileAttributes.Archive); + if (rc.IsFailure()) return rc; + + long remaining = size; + + for (int i = 0; remaining > 0; i++) + { + long fileSize = Math.Min(SubFileSize, remaining); + string fileName = GetSubFilePath(path, i); + + Result createSubFileResult = BaseFileSystem.CreateFile(fileName, fileSize, CreateFileOptions.None); + + if (createSubFileResult.IsFailure()) + { + BaseFileSystem.DeleteDirectoryRecursively(path); + return createSubFileResult; + } + + remaining -= fileSize; + } + + return Result.Success; + } + + protected override Result DeleteDirectoryImpl(string path) + { + path = PathTools.Normalize(path); + + if (IsConcatenationFile(path)) + { + return ResultFs.PathNotFound.Log(); + } + + return BaseFileSystem.DeleteDirectory(path); + } + + protected override Result DeleteDirectoryRecursivelyImpl(string path) + { + path = PathTools.Normalize(path); + + if (IsConcatenationFile(path)) return ResultFs.PathNotFound.Log(); + + return BaseFileSystem.DeleteDirectoryRecursively(path); + } + + protected override Result CleanDirectoryRecursivelyImpl(string path) + { + path = PathTools.Normalize(path); + + if (IsConcatenationFile(path)) return ResultFs.PathNotFound.Log(); + + return BaseFileSystem.CleanDirectoryRecursively(path); + } + + protected override Result DeleteFileImpl(string path) + { + path = PathTools.Normalize(path); + + if (!IsConcatenationFile(path)) + { + return BaseFileSystem.DeleteFile(path); + } + + int count = GetSubFileCount(path); + + for (int i = 0; i < count; i++) + { + Result rc = BaseFileSystem.DeleteFile(GetSubFilePath(path, i)); + if (rc.IsFailure()) return rc; + } + + return BaseFileSystem.DeleteDirectory(path); + } + + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) + { + directory = default; + path = PathTools.Normalize(path); + + if (IsConcatenationFile(path)) + { + return ResultFs.PathNotFound.Log(); + } + + Result rc = BaseFileSystem.OpenDirectory(out IDirectory parentDir, path, OpenDirectoryMode.All); + if (rc.IsFailure()) return rc; + + directory = new ConcatenationDirectory(this, BaseFileSystem, parentDir, mode, path); + return Result.Success; + } + + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) + { + file = default; + path = PathTools.Normalize(path); + + if (!IsConcatenationFile(path)) + { + return BaseFileSystem.OpenFile(out file, path, mode); + } + + int fileCount = GetSubFileCount(path); + + var files = new List(); + + for (int i = 0; i < fileCount; i++) + { + string filePath = GetSubFilePath(path, i); + + Result rc = BaseFileSystem.OpenFile(out IFile subFile, filePath, mode); + if (rc.IsFailure()) return rc; + + files.Add(subFile); + } + + file = new ConcatenationFile(BaseFileSystem, path, files, SubFileSize, mode); + return Result.Success; + } + + protected override Result RenameDirectoryImpl(string oldPath, string newPath) + { + oldPath = PathTools.Normalize(oldPath); + newPath = PathTools.Normalize(newPath); + + if (IsConcatenationFile(oldPath)) + { + return ResultFs.PathNotFound.Log(); + } + + return BaseFileSystem.RenameDirectory(oldPath, newPath); + } + + protected override Result RenameFileImpl(string oldPath, string newPath) + { + oldPath = PathTools.Normalize(oldPath); + newPath = PathTools.Normalize(newPath); + + if (IsConcatenationFile(oldPath)) + { + return BaseFileSystem.RenameDirectory(oldPath, newPath); + } + else + { + return BaseFileSystem.RenameFile(oldPath, newPath); + } + } + + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) + { + path = PathTools.Normalize(path); + + if (IsConcatenationFile(path)) + { + entryType = DirectoryEntryType.File; + return Result.Success; + } + + return BaseFileSystem.GetEntryType(out entryType, path); + } + + protected override Result GetFreeSpaceSizeImpl(out long freeSpace, string path) + { + return BaseFileSystem.GetFreeSpaceSize(out freeSpace, path); + } + + protected override Result GetTotalSpaceSizeImpl(out long totalSpace, string path) + { + return BaseFileSystem.GetTotalSpaceSize(out totalSpace, path); + } + + protected override Result GetFileTimeStampRawImpl(out FileTimeStampRaw timeStamp, string path) + { + return BaseFileSystem.GetFileTimeStampRaw(out timeStamp, path); + } + + protected override Result CommitImpl() + { + return BaseFileSystem.Commit(); + } + + protected override Result QueryEntryImpl(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, string path) + { + if (queryId != QueryId.MakeConcatFile) return ResultFs.UnsupportedOperationInConcatFsQueryEntry.Log(); + + return SetConcatenationFileAttribute(path); + } + + private int GetSubFileCount(string dirPath) + { + int count = 0; + + while (BaseFileSystem.FileExists(GetSubFilePath(dirPath, count))) + { + count++; + } + + return count; + } + + internal static string GetSubFilePath(string dirPath, int index) + { + return $"{dirPath}/{index:D2}"; + } + + internal long GetConcatenationFileSize(string path) + { + int fileCount = GetSubFileCount(path); + long size = 0; + + for (int i = 0; i < fileCount; i++) + { + BaseFileSystem.GetFileSize(out long fileSize, GetSubFilePath(path, i)).ThrowIfFailure(); + size += fileSize; + } + + return size; + } + } +} diff --git a/src/LibHac/Fs/ConcatenationStorage.cs b/src/LibHac/FsSystem/ConcatenationStorage.cs similarity index 59% rename from src/LibHac/Fs/ConcatenationStorage.cs rename to src/LibHac/FsSystem/ConcatenationStorage.cs index a6a6978a..13ef9c5e 100644 --- a/src/LibHac/Fs/ConcatenationStorage.cs +++ b/src/LibHac/FsSystem/ConcatenationStorage.cs @@ -1,34 +1,42 @@ using System; using System.Collections.Generic; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class ConcatenationStorage : StorageBase { private ConcatSource[] Sources { get; } - private long _length; + private long Length { get; } + private bool LeaveOpen { get; } public ConcatenationStorage(IList sources, bool leaveOpen) { Sources = new ConcatSource[sources.Count]; - if (!leaveOpen) ToDispose.AddRange(sources); + LeaveOpen = leaveOpen; long length = 0; for (int i = 0; i < sources.Count; i++) { - if (sources[i].GetSize() < 0) throw new ArgumentException("Sources must have an explicit length."); - Sources[i] = new ConcatSource(sources[i], length, sources[i].GetSize()); - length += sources[i].GetSize(); + sources[i].GetSize(out long sourceSize).ThrowIfFailure(); + + if (sourceSize < 0) throw new ArgumentException("Sources must have an explicit length."); + Sources[i] = new ConcatSource(sources[i], length, sourceSize); + length += sourceSize; } - _length = length; + Length = length; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { long inPos = offset; int outPos = 0; int remaining = destination.Length; + + if (!IsRangeValid(offset, destination.Length, Length)) + return ResultFs.ValueOutOfRange.Log(); + int sourceIndex = FindSource(inPos); while (remaining > 0) @@ -38,20 +46,28 @@ namespace LibHac.Fs long entryRemain = entry.StartOffset + entry.Size - inPos; int bytesToRead = (int)Math.Min(entryRemain, remaining); - entry.Storage.Read(destination.Slice(outPos, bytesToRead), entryPos); + + Result rc = entry.Storage.Read(entryPos, destination.Slice(outPos, bytesToRead)); + if (rc.IsFailure()) return rc; outPos += bytesToRead; inPos += bytesToRead; remaining -= bytesToRead; sourceIndex++; } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { long inPos = offset; int outPos = 0; int remaining = source.Length; + + if (!IsRangeValid(offset, source.Length, Length)) + return ResultFs.ValueOutOfRange.Log(); + int sourceIndex = FindSource(inPos); while (remaining > 0) @@ -61,28 +77,58 @@ namespace LibHac.Fs long entryRemain = entry.StartOffset + entry.Size - inPos; int bytesToWrite = (int)Math.Min(entryRemain, remaining); - entry.Storage.Write(source.Slice(outPos, bytesToWrite), entryPos); + + Result rc = entry.Storage.Write(entryPos, source.Slice(outPos, bytesToWrite)); + if (rc.IsFailure()) return rc; outPos += bytesToWrite; inPos += bytesToWrite; remaining -= bytesToWrite; sourceIndex++; } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { foreach (ConcatSource source in Sources) { - source.Storage.Flush(); + Result rc = source.Storage.Flush(); + if (rc.IsFailure()) return rc; + } + + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.NotImplemented.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!LeaveOpen && Sources != null) + { + foreach (ConcatSource source in Sources) + { + source?.Storage?.Dispose(); + } + } } } - public override long GetSize() => _length; - private int FindSource(long offset) { - if (offset < 0 || offset >= _length) + if (offset < 0 || offset >= Length) throw new ArgumentOutOfRangeException(nameof(offset), offset, "The Storage does not contain this offset."); int lo = 0; diff --git a/src/LibHac/Fs/ConcatenationStorageBuilder.cs b/src/LibHac/FsSystem/ConcatenationStorageBuilder.cs similarity index 90% rename from src/LibHac/Fs/ConcatenationStorageBuilder.cs rename to src/LibHac/FsSystem/ConcatenationStorageBuilder.cs index 3ea5f670..afe464bd 100644 --- a/src/LibHac/Fs/ConcatenationStorageBuilder.cs +++ b/src/LibHac/FsSystem/ConcatenationStorageBuilder.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class ConcatenationStorageBuilder { @@ -41,8 +42,10 @@ namespace LibHac.Fs sources.Add(new NullStorage(paddingNeeded)); } + segment.Storage.GetSize(out long segmentSize).ThrowIfFailure(); + sources.Add(segment.Storage); - offset = segment.Offset + segment.Storage.GetSize(); + offset = segment.Offset + segmentSize; } return new ConcatenationStorage(sources, true); diff --git a/src/LibHac/Fs/DeltaFragment.cs b/src/LibHac/FsSystem/Delta.cs similarity index 66% rename from src/LibHac/Fs/DeltaFragment.cs rename to src/LibHac/FsSystem/Delta.cs index 77711ebd..a3869dbe 100644 --- a/src/LibHac/Fs/DeltaFragment.cs +++ b/src/LibHac/FsSystem/Delta.cs @@ -1,34 +1,36 @@ using System; using System.Collections.Generic; using System.IO; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { - public class DeltaFragment + public class Delta { private const string Ndv0Magic = "NDV0"; - private IStorage Original { get; set; } - private IStorage Delta { get; } - public DeltaFragmentHeader Header { get; } - private List Segments { get; } = new List(); + private IStorage OriginalStorage { get; set; } + private IStorage DeltaStorage { get; } + public DeltaHeader Header { get; } + private List Segments { get; } = new List(); - public DeltaFragment(IStorage delta, IStorage originalData) : this(delta) + public Delta(IStorage deltaStorage, IStorage originalData) : this(deltaStorage) { SetBaseStorage(originalData); } - public DeltaFragment(IStorage delta) + public Delta(IStorage deltaStorage) { - Delta = delta; + DeltaStorage = deltaStorage; + deltaStorage.GetSize(out long deltaSize).ThrowIfFailure(); - if (Delta.GetSize() < 0x40) throw new InvalidDataException("Delta file is too small."); + if (deltaSize < 0x40) throw new InvalidDataException("Delta file is too small."); - Header = new DeltaFragmentHeader(delta.AsFile(OpenMode.Read)); + Header = new DeltaHeader(deltaStorage.AsFile(OpenMode.Read)); if (Header.Magic != Ndv0Magic) throw new InvalidDataException("NDV0 magic value is missing."); - long fragmentSize = Header.FragmentHeaderSize + Header.FragmentBodySize; - if (Delta.GetSize() < fragmentSize) + long fragmentSize = Header.HeaderSize + Header.BodySize; + if (deltaSize < fragmentSize) { throw new InvalidDataException($"Delta file is smaller than the header indicates. (0x{fragmentSize} bytes)"); } @@ -38,9 +40,10 @@ namespace LibHac.Fs public void SetBaseStorage(IStorage baseStorage) { - Original = baseStorage; + OriginalStorage = baseStorage; + baseStorage.GetSize(out long storageSize).ThrowIfFailure(); - if (Original.GetSize() != Header.OriginalSize) + if (storageSize != Header.OriginalSize) { throw new InvalidDataException($"Original file size does not match the size in the delta header. (0x{Header.OriginalSize} bytes)"); } @@ -48,13 +51,13 @@ namespace LibHac.Fs public IStorage GetPatchedStorage() { - if (Original == null) throw new InvalidOperationException("Cannot apply a delta patch without a base file."); + if (OriginalStorage == null) throw new InvalidOperationException("Cannot apply a delta patch without a base file."); var storages = new List(); - foreach (DeltaFragmentSegment segment in Segments) + foreach (DeltaSegment segment in Segments) { - IStorage source = segment.IsInOriginal ? Original : Delta; + IStorage source = segment.IsInOriginal ? OriginalStorage : DeltaStorage; // todo Do this without tons of SubStorages IStorage sub = source.Slice(segment.SourceOffset, segment.Size); @@ -67,9 +70,9 @@ namespace LibHac.Fs private void ParseDeltaStructure() { - var reader = new FileReader(Delta.AsFile(OpenMode.Read)); + var reader = new FileReader(DeltaStorage.AsFile(OpenMode.Read)); - reader.Position = Header.FragmentHeaderSize; + reader.Position = Header.HeaderSize; long offset = 0; @@ -79,7 +82,7 @@ namespace LibHac.Fs if (seek > 0) { - var segment = new DeltaFragmentSegment() + var segment = new DeltaSegment() { SourceOffset = offset, Size = seek, @@ -92,7 +95,7 @@ namespace LibHac.Fs if (size > 0) { - var segment = new DeltaFragmentSegment() + var segment = new DeltaSegment() { SourceOffset = reader.Position, Size = size, @@ -131,30 +134,30 @@ namespace LibHac.Fs } } - internal class DeltaFragmentSegment + internal class DeltaSegment { public long SourceOffset { get; set; } public int Size { get; set; } public bool IsInOriginal { get; set; } } - public class DeltaFragmentHeader + public class DeltaHeader { public string Magic { get; } public long OriginalSize { get; } public long NewSize { get; } - public long FragmentHeaderSize { get; } - public long FragmentBodySize { get; } + public long HeaderSize { get; } + public long BodySize { get; } - public DeltaFragmentHeader(IFile header) + public DeltaHeader(IFile header) { var reader = new FileReader(header); Magic = reader.ReadAscii(4); OriginalSize = reader.ReadInt64(8); NewSize = reader.ReadInt64(); - FragmentHeaderSize = reader.ReadInt64(); - FragmentBodySize = reader.ReadInt64(); + HeaderSize = reader.ReadInt64(); + BodySize = reader.ReadInt64(); } } } diff --git a/src/LibHac/FsSystem/DirectorySaveDataFile.cs b/src/LibHac/FsSystem/DirectorySaveDataFile.cs new file mode 100644 index 00000000..de54607d --- /dev/null +++ b/src/LibHac/FsSystem/DirectorySaveDataFile.cs @@ -0,0 +1,52 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class DirectorySaveDataFile : FileBase + { + private IFile BaseFile { get; } + private DirectorySaveDataFileSystem ParentFs { get; } + private OpenMode Mode { get; } + + public DirectorySaveDataFile(DirectorySaveDataFileSystem parentFs, IFile baseFile, OpenMode mode) + { + ParentFs = parentFs; + BaseFile = baseFile; + Mode = mode; + } + + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) + { + return BaseFile.Read(out bytesRead, offset, destination, options); + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) + { + return BaseFile.Write(offset, source, options); + } + + protected override Result FlushImpl() + { + return BaseFile.Flush(); + } + + protected override Result GetSizeImpl(out long size) + { + return BaseFile.GetSize(out size); + } + + protected override Result SetSizeImpl(long size) + { + return BaseFile.SetSize(size); + } + + protected override void Dispose(bool disposing) + { + if (Mode.HasFlag(OpenMode.Write)) + { + ParentFs.NotifyCloseWritableFile(); + } + } + } +} diff --git a/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs new file mode 100644 index 00000000..93e03091 --- /dev/null +++ b/src/LibHac/FsSystem/DirectorySaveDataFileSystem.cs @@ -0,0 +1,203 @@ +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class DirectorySaveDataFileSystem : FileSystemBase + { + private const string CommittedDir = "/0/"; + private const string WorkingDir = "/1/"; + private const string SyncDir = "/_/"; + + private IFileSystem BaseFs { get; } + private object Locker { get; } = new object(); + private int OpenWritableFileCount { get; set; } + + public DirectorySaveDataFileSystem(IFileSystem baseFileSystem) + { + BaseFs = baseFileSystem; + + if (!BaseFs.DirectoryExists(WorkingDir)) + { + BaseFs.CreateDirectory(WorkingDir); + BaseFs.EnsureDirectoryExists(CommittedDir); + } + + if (BaseFs.DirectoryExists(CommittedDir)) + { + SynchronizeDirectory(WorkingDir, CommittedDir); + } + else + { + SynchronizeDirectory(SyncDir, WorkingDir); + BaseFs.RenameDirectory(SyncDir, CommittedDir); + } + } + + protected override Result CreateDirectoryImpl(string path) + { + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + return BaseFs.CreateDirectory(fullPath); + } + } + + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) + { + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + return BaseFs.CreateFile(fullPath, size, options); + } + } + + protected override Result DeleteDirectoryImpl(string path) + { + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + return BaseFs.DeleteDirectory(fullPath); + } + } + + protected override Result DeleteDirectoryRecursivelyImpl(string path) + { + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + return BaseFs.DeleteDirectoryRecursively(fullPath); + } + } + + protected override Result CleanDirectoryRecursivelyImpl(string path) + { + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + return BaseFs.CleanDirectoryRecursively(fullPath); + } + } + + protected override Result DeleteFileImpl(string path) + { + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + return BaseFs.DeleteFile(fullPath); + } + } + + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) + { + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + return BaseFs.OpenDirectory(out directory, fullPath, mode); + } + } + + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) + { + file = default; + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + Result rc = BaseFs.OpenFile(out IFile baseFile, fullPath, mode); + if (rc.IsFailure()) return rc; + + file = new DirectorySaveDataFile(this, baseFile, mode); + + if (mode.HasFlag(OpenMode.Write)) + { + OpenWritableFileCount++; + } + + return Result.Success; + } + } + + protected override Result RenameDirectoryImpl(string oldPath, string newPath) + { + string fullOldPath = GetFullPath(PathTools.Normalize(oldPath)); + string fullNewPath = GetFullPath(PathTools.Normalize(newPath)); + + lock (Locker) + { + return BaseFs.RenameDirectory(fullOldPath, fullNewPath); + } + } + + protected override Result RenameFileImpl(string oldPath, string newPath) + { + string fullOldPath = GetFullPath(PathTools.Normalize(oldPath)); + string fullNewPath = GetFullPath(PathTools.Normalize(newPath)); + + lock (Locker) + { + return BaseFs.RenameFile(fullOldPath, fullNewPath); + } + } + + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) + { + string fullPath = GetFullPath(PathTools.Normalize(path)); + + lock (Locker) + { + return BaseFs.GetEntryType(out entryType, fullPath); + } + } + + protected override Result CommitImpl() + { + lock (Locker) + { + if (OpenWritableFileCount > 0) + { + // All files must be closed before commiting save data. + return ResultFs.WritableFileOpen.Log(); + } + + Result rc = SynchronizeDirectory(SyncDir, WorkingDir); + if (rc.IsFailure()) return rc; + + rc = BaseFs.DeleteDirectoryRecursively(CommittedDir); + if (rc.IsFailure()) return rc; + + return BaseFs.RenameDirectory(SyncDir, CommittedDir); + } + } + + private string GetFullPath(string path) + { + return PathTools.Normalize(PathTools.Combine(WorkingDir, path)); + } + + private Result SynchronizeDirectory(string dest, string src) + { + Result rc = BaseFs.DeleteDirectoryRecursively(dest); + if (rc.IsFailure() && rc != ResultFs.PathNotFound) return rc; + + rc = BaseFs.CreateDirectory(dest); + if (rc.IsFailure()) return rc; + + return BaseFs.CopyDirectory(BaseFs, src, dest); + } + + internal void NotifyCloseWritableFile() + { + lock (Locker) + { + OpenWritableFileCount--; + } + } + } +} diff --git a/src/LibHac/FsSystem/DirectoryUtils.cs b/src/LibHac/FsSystem/DirectoryUtils.cs new file mode 100644 index 00000000..f28e5896 --- /dev/null +++ b/src/LibHac/FsSystem/DirectoryUtils.cs @@ -0,0 +1,86 @@ +using System; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public static class DirectoryUtils + { + public delegate Result Blah(ReadOnlySpan path, ref DirectoryEntry entry); + + public static Result IterateDirectoryRecursivelyInternal(IFileSystem fs, Span workPath, + ref DirectoryEntry entry, Blah onEnterDir, Blah onExitDir, Blah onFile) + { + string currentPath = Util.GetUtf8StringNullTerminated(workPath); + + Result rc = fs.OpenDirectory(out IDirectory _, currentPath, OpenDirectoryMode.All); + if (rc.IsFailure()) return rc; + + onFile(workPath, ref entry); + + return Result.Success; + } + + public static Result IterateDirectoryRecursively(IFileSystem fs, ReadOnlySpan path, Blah onEnterDir, Blah onExitDir, Blah onFile) + { + return Result.Success; + } + + public static Result CopyDirectoryRecursively(IFileSystem sourceFs, IFileSystem destFs, string sourcePath, + string destPath) + { + return Result.Success; + } + + public static Result CopyFile(IFileSystem destFs, IFileSystem sourceFs, ReadOnlySpan destParentPath, + ReadOnlySpan sourcePath, ref DirectoryEntry dirEntry, Span copyBuffer) + { + IFile srcFile = null; + IFile dstFile = null; + + try + { + Result rc = sourceFs.OpenFile(out srcFile, StringUtils.Utf8ZToString(sourcePath), OpenMode.Read); + if (rc.IsFailure()) return rc; + + FsPath dstPath = default; + int dstPathLen = StringUtils.Concat(dstPath.Str, destParentPath); + dstPathLen = StringUtils.Concat(dstPath.Str, dstPathLen, dirEntry.Name); + + if (dstPathLen > FsPath.MaxLength) + { + throw new ArgumentException(); + } + + string dstPathStr = StringUtils.Utf8ZToString(dstPath.Str); + + rc = destFs.CreateFile(dstPathStr, dirEntry.Size, CreateFileOptions.None); + if (rc.IsFailure()) return rc; + + rc = destFs.OpenFile(out dstFile, dstPathStr, OpenMode.Write); + if (rc.IsFailure()) return rc; + + long fileSize = dirEntry.Size; + long offset = 0; + + while (offset < fileSize) + { + rc = srcFile.Read(out long bytesRead, offset, copyBuffer, ReadOption.None); + if (rc.IsFailure()) return rc; + + rc = dstFile.Write(offset, copyBuffer.Slice(0, (int)bytesRead), WriteOption.None); + if (rc.IsFailure()) return rc; + + offset += bytesRead; + } + + return Result.Success; + } + finally + { + srcFile?.Dispose(); + dstFile?.Dispose(); + } + } + } +} diff --git a/src/LibHac/Fs/FileReader.cs b/src/LibHac/FsSystem/FileReader.cs similarity index 93% rename from src/LibHac/Fs/FileReader.cs rename to src/LibHac/FsSystem/FileReader.cs index 5a7779f1..b1c79db8 100644 --- a/src/LibHac/Fs/FileReader.cs +++ b/src/LibHac/FsSystem/FileReader.cs @@ -2,8 +2,9 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class FileReader { @@ -31,7 +32,7 @@ namespace LibHac.Fs { Debug.Assert(count <= BufferSize); - _file.Read(_buffer.AsSpan(0, count), _start + offset); + _file.Read(out long _, _start + offset, _buffer.AsSpan(0, count)).ThrowIfFailure(); if (updatePosition) Position = offset + count; } @@ -101,7 +102,7 @@ namespace LibHac.Fs public long ReadInt64(long offset, bool updatePosition) { FillBuffer(offset, sizeof(long), updatePosition); - + return MemoryMarshal.Read(_buffer); } @@ -121,16 +122,16 @@ namespace LibHac.Fs public byte[] ReadBytes(long offset, int length, bool updatePosition) { - var result = new byte[length]; - _file.Read(result, offset); + var bytes = new byte[length]; + _file.Read(out long _, offset, bytes).ThrowIfFailure(); if (updatePosition) Position = offset + length; - return result; + return bytes; } public void ReadBytes(Span destination, long offset, bool updatePosition) { - _file.Read(destination, offset); + _file.Read(out long _, offset, destination).ThrowIfFailure(); if (updatePosition) Position = offset + destination.Length; } @@ -138,7 +139,7 @@ namespace LibHac.Fs public string ReadAscii(long offset, int length, bool updatePosition) { var bytes = new byte[length]; - _file.Read(bytes, offset); + _file.Read(out long _, offset, bytes).ThrowIfFailure(); if (updatePosition) Position = offset + length; return Encoding.ASCII.GetString(bytes); diff --git a/src/LibHac/FsSystem/FileStorage.cs b/src/LibHac/FsSystem/FileStorage.cs new file mode 100644 index 00000000..d9cc1f88 --- /dev/null +++ b/src/LibHac/FsSystem/FileStorage.cs @@ -0,0 +1,40 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class FileStorage : StorageBase + { + private IFile BaseFile { get; } + + public FileStorage(IFile baseFile) + { + BaseFile = baseFile; + } + + protected override Result ReadImpl(long offset, Span destination) + { + return BaseFile.Read(out long _, offset, destination); + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source) + { + return BaseFile.Write(offset, source); + } + + protected override Result FlushImpl() + { + return BaseFile.Flush(); + } + + protected override Result GetSizeImpl(out long size) + { + return BaseFile.GetSize(out size); + } + + protected override Result SetSizeImpl(long size) + { + return BaseFile.SetSize(size); + } + } +} diff --git a/src/LibHac/Fs/FileSystemExtensions.cs b/src/LibHac/FsSystem/FileSystemExtensions.cs similarity index 50% rename from src/LibHac/Fs/FileSystemExtensions.cs rename to src/LibHac/FsSystem/FileSystemExtensions.cs index cc88706b..cbe7478d 100644 --- a/src/LibHac/Fs/FileSystemExtensions.cs +++ b/src/LibHac/FsSystem/FileSystemExtensions.cs @@ -2,88 +2,94 @@ using System.Buffers; using System.Collections.Generic; using System.IO; +using LibHac.Common; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public static class FileSystemExtensions { - public static void CopyDirectory(this IDirectory source, IDirectory dest, IProgressReport logger = null, CreateFileOptions options = CreateFileOptions.None) + public static Result CopyDirectory(this IFileSystem sourceFs, IFileSystem destFs, string sourcePath, string destPath, + IProgressReport logger = null, CreateFileOptions options = CreateFileOptions.None) { - IFileSystem sourceFs = source.ParentFileSystem; - IFileSystem destFs = dest.ParentFileSystem; + Result rc; - foreach (DirectoryEntry entry in source.Read()) + foreach (DirectoryEntryEx entry in sourceFs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) { - string subSrcPath = PathTools.Normalize(PathTools.Combine(source.FullPath, entry.Name)); - string subDstPath = PathTools.Normalize(PathTools.Combine(dest.FullPath, entry.Name)); + string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); + string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); if (entry.Type == DirectoryEntryType.Directory) { destFs.EnsureDirectoryExists(subDstPath); - IDirectory subSrcDir = sourceFs.OpenDirectory(subSrcPath, OpenDirectoryMode.All); - IDirectory subDstDir = destFs.OpenDirectory(subDstPath, OpenDirectoryMode.All); - subSrcDir.CopyDirectory(subDstDir, logger, options); + rc = sourceFs.CopyDirectory(destFs, subSrcPath, subDstPath, logger, options); + if (rc.IsFailure()) return rc; } if (entry.Type == DirectoryEntryType.File) { destFs.CreateOrOverwriteFile(subDstPath, entry.Size, options); - using (IFile srcFile = sourceFs.OpenFile(subSrcPath, OpenMode.Read)) - using (IFile dstFile = destFs.OpenFile(subDstPath, OpenMode.Write | OpenMode.Append)) + rc = sourceFs.OpenFile(out IFile srcFile, subSrcPath, OpenMode.Read); + if (rc.IsFailure()) return rc; + + using (srcFile) { - logger?.LogMessage(subSrcPath); - srcFile.CopyTo(dstFile, logger); + rc = destFs.OpenFile(out IFile dstFile, subDstPath, OpenMode.Write | OpenMode.AllowAppend); + if (rc.IsFailure()) return rc; + + using (dstFile) + { + logger?.LogMessage(subSrcPath); + srcFile.CopyTo(dstFile, logger); + } } } } - } - public static void CopyFileSystem(this IFileSystem source, IFileSystem dest, IProgressReport logger = null, CreateFileOptions options = CreateFileOptions.None) - { - IDirectory sourceRoot = source.OpenDirectory("/", OpenDirectoryMode.All); - IDirectory destRoot = dest.OpenDirectory("/", OpenDirectoryMode.All); - - sourceRoot.CopyDirectory(destRoot, logger, options); + return Result.Success; } public static void Extract(this IFileSystem source, string destinationPath, IProgressReport logger = null) { var destFs = new LocalFileSystem(destinationPath); - source.CopyFileSystem(destFs, logger); + source.CopyDirectory(destFs, "/", "/", logger); } - public static IEnumerable EnumerateEntries(this IFileSystem fileSystem) + public static IEnumerable EnumerateEntries(this IFileSystem fileSystem) { - return fileSystem.EnumerateEntries("*"); + return fileSystem.EnumerateEntries("/", "*"); } - public static IEnumerable EnumerateEntries(this IFileSystem fileSystem, string searchPattern) + public static IEnumerable EnumerateEntries(this IFileSystem fileSystem, string path, string searchPattern) { - return fileSystem.EnumerateEntries(searchPattern, SearchOptions.RecurseSubdirectories); + return fileSystem.EnumerateEntries(path, searchPattern, SearchOptions.RecurseSubdirectories); } - public static IEnumerable EnumerateEntries(this IFileSystem fileSystem, string searchPattern, SearchOptions searchOptions) + public static IEnumerable EnumerateEntries(this IFileSystem fileSystem, string searchPattern, SearchOptions searchOptions) { - return fileSystem.OpenDirectory("/", OpenDirectoryMode.All).EnumerateEntries(searchPattern, searchOptions); + return EnumerateEntries(fileSystem, "/", searchPattern, searchOptions); } - public static IEnumerable EnumerateEntries(this IDirectory directory) - { - return directory.EnumerateEntries("*", SearchOptions.Default); - } - - public static IEnumerable EnumerateEntries(this IDirectory directory, string searchPattern, SearchOptions searchOptions) + public static IEnumerable EnumerateEntries(this IFileSystem fileSystem, string path, string searchPattern, SearchOptions searchOptions) { bool ignoreCase = searchOptions.HasFlag(SearchOptions.CaseInsensitive); bool recurse = searchOptions.HasFlag(SearchOptions.RecurseSubdirectories); - IFileSystem fs = directory.ParentFileSystem; + IFileSystem fs = fileSystem; + DirectoryEntry dirEntry = default; - foreach (DirectoryEntry entry in directory.Read()) + fileSystem.OpenDirectory(out IDirectory directory, path, OpenDirectoryMode.All).ThrowIfFailure(); + + while (true) { + directory.Read(out long entriesRead, SpanHelpers.AsSpan(ref dirEntry)).ThrowIfFailure(); + if (entriesRead == 0) break; + + DirectoryEntryEx entry = GetDirectoryEntryEx(ref dirEntry, path); + if (PathTools.MatchesPattern(searchPattern, entry.Name, ignoreCase)) { yield return entry; @@ -91,29 +97,48 @@ namespace LibHac.Fs if (entry.Type != DirectoryEntryType.Directory || !recurse) continue; - IDirectory subDir = fs.OpenDirectory(PathTools.Combine(directory.FullPath, entry.Name), OpenDirectoryMode.All); + IEnumerable subEntries = + fs.EnumerateEntries(PathTools.Combine(path, entry.Name), searchPattern, + searchOptions); - foreach (DirectoryEntry subEntry in subDir.EnumerateEntries(searchPattern, searchOptions)) + foreach (DirectoryEntryEx subEntry in subEntries) { yield return subEntry; } } } + internal static DirectoryEntryEx GetDirectoryEntryEx(ref DirectoryEntry entry, string parentPath) + { + string name = StringUtils.Utf8ZToString(entry.Name); + string path = PathTools.Combine(parentPath, name); + + var entryEx = new DirectoryEntryEx(name, path, entry.Type, entry.Size); + entryEx.Attributes = entry.Attributes; + + return entryEx; + } + public static void CopyTo(this IFile file, IFile dest, IProgressReport logger = null) { const int bufferSize = 0x8000; - logger?.SetTotal(file.GetSize()); + + file.GetSize(out long fileSize).ThrowIfFailure(); + + logger?.SetTotal(fileSize); byte[] buffer = ArrayPool.Shared.Rent(bufferSize); try { long inOffset = 0; - int bytesRead; - while ((bytesRead = file.Read(buffer, inOffset)) != 0) + // todo: use result for loop condition + while (true) { - dest.Write(buffer.AsSpan(0, bytesRead), inOffset); + file.Read(out long bytesRead, inOffset, buffer).ThrowIfFailure(); + if (bytesRead == 0) break; + + dest.Write(inOffset, buffer.AsSpan(0, (int)bytesRead)).ThrowIfFailure(); inOffset += bytesRead; logger?.ReportAdd(bytesRead); } @@ -127,23 +152,23 @@ namespace LibHac.Fs public static IStorage AsStorage(this IFile file) => new FileStorage(file); public static Stream AsStream(this IFile file) => new NxFileStream(file, true); - public static Stream AsStream(this IFile file, bool keepOpen) => new NxFileStream(file, keepOpen); + public static Stream AsStream(this IFile file, OpenMode mode, bool keepOpen) => new NxFileStream(file, mode, keepOpen); public static IFile AsIFile(this Stream stream, OpenMode mode) => new StreamFile(stream, mode); public static int GetEntryCount(this IFileSystem fs, OpenDirectoryMode mode) { - return fs.OpenDirectory("/", OpenDirectoryMode.All).GetEntryCountRecursive(mode); + return GetEntryCountRecursive(fs, "/", mode); } - public static int GetEntryCountRecursive(this IDirectory directory, OpenDirectoryMode mode) + public static int GetEntryCountRecursive(this IFileSystem fs, string path, OpenDirectoryMode mode) { int count = 0; - foreach (DirectoryEntry entry in directory.EnumerateEntries()) + foreach (DirectoryEntryEx entry in fs.EnumerateEntries(path, "*")) { - if (entry.Type == DirectoryEntryType.Directory && (mode & OpenDirectoryMode.Directories) != 0 || - entry.Type == DirectoryEntryType.File && (mode & OpenDirectoryMode.Files) != 0) + if (entry.Type == DirectoryEntryType.Directory && (mode & OpenDirectoryMode.Directory) != 0 || + entry.Type == DirectoryEntryType.File && (mode & OpenDirectoryMode.File) != 0) { count++; } @@ -157,30 +182,36 @@ namespace LibHac.Fs return (NxFileAttributes)(((int)attributes >> 4) & 3); } + public static FileAttributes ToFatAttributes(this NxFileAttributes attributes) + { + return (FileAttributes)(((int)attributes & 3) << 4); + } + public static FileAttributes ApplyNxAttributes(this FileAttributes attributes, NxFileAttributes nxAttributes) { - var nxAttributeBits = (FileAttributes)(((int)nxAttributes & 3) << 4); - return attributes | nxAttributeBits; + // The only 2 bits from FileAttributes that are used in NxFileAttributes + const int mask = 3 << 4; + + FileAttributes oldAttributes = attributes & (FileAttributes)mask; + return oldAttributes | nxAttributes.ToFatAttributes(); } public static void SetConcatenationFileAttribute(this IFileSystem fs, string path) { - fs.QueryEntry(Span.Empty, Span.Empty, path, QueryId.MakeConcatFile); + fs.QueryEntry(Span.Empty, Span.Empty, QueryId.MakeConcatFile, path); } - public static void CleanDirectoryRecursivelyGeneric(IDirectory directory) + public static void CleanDirectoryRecursivelyGeneric(IFileSystem fileSystem, string path) { - IFileSystem fs = directory.ParentFileSystem; + IFileSystem fs = fileSystem; - foreach (DirectoryEntry entry in directory.Read()) + foreach (DirectoryEntryEx entry in fileSystem.EnumerateEntries(path, "*")) { - string subPath = PathTools.Combine(directory.FullPath, entry.Name); + string subPath = PathTools.Combine(path, entry.Name); if (entry.Type == DirectoryEntryType.Directory) { - IDirectory subDir = fs.OpenDirectory(subPath, OpenDirectoryMode.All); - - CleanDirectoryRecursivelyGeneric(subDir); + CleanDirectoryRecursivelyGeneric(fileSystem, subPath); fs.DeleteDirectory(subPath); } else if (entry.Type == DirectoryEntryType.File) @@ -190,24 +221,28 @@ namespace LibHac.Fs } } - public static int Read(this IFile file, Span destination, long offset) + public static Result Read(this IFile file, out long bytesRead, long offset, Span destination) { - return file.Read(destination, offset, ReadOption.None); + return file.Read(out bytesRead, offset, destination, ReadOption.None); } - public static void Write(this IFile file, ReadOnlySpan source, long offset) + public static Result Write(this IFile file, long offset, ReadOnlySpan source) { - file.Write(source, offset, WriteOption.None); + return file.Write(offset, source, WriteOption.None); } public static bool DirectoryExists(this IFileSystem fs, string path) { - return fs.GetEntryType(path) == DirectoryEntryType.Directory; + Result rc = fs.GetEntryType(out DirectoryEntryType type, path); + + return (rc.IsSuccess() && type == DirectoryEntryType.Directory); } public static bool FileExists(this IFileSystem fs, string path) { - return fs.GetEntryType(path) == DirectoryEntryType.File; + Result rc = fs.GetEntryType(out DirectoryEntryType type, path); + + return (rc.IsSuccess() && type == DirectoryEntryType.File); } public static void EnsureDirectoryExists(this IFileSystem fs, string path) diff --git a/src/LibHac/FsSystem/FsPath.cs b/src/LibHac/FsSystem/FsPath.cs new file mode 100644 index 00000000..6da1475e --- /dev/null +++ b/src/LibHac/FsSystem/FsPath.cs @@ -0,0 +1,36 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = MaxLength + 1)] + public struct FsPath + { + internal const int MaxLength = 0x300; + +#if DEBUG + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding100 Padding000; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding100 Padding100; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Padding100 Padding200; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly byte Padding300; +#endif + + public Span Str => SpanHelpers.AsByteSpan(ref this); + + public static Result FromSpan(out FsPath fsPath, ReadOnlySpan path) + { + fsPath = new FsPath(); + + U8StringBuilder builder = new U8StringBuilder(fsPath.Str).Append(path); + + return builder.Overflowed ? ResultFs.TooLongPath : Result.Success; + } + + public static implicit operator U8Span(FsPath value) => new U8Span(value.Str); + public override string ToString() => StringUtils.Utf8ZToString(Str); + } +} diff --git a/src/LibHac/Fs/HierarchicalIntegrityVerificationStorage.cs b/src/LibHac/FsSystem/HierarchicalIntegrityVerificationStorage.cs similarity index 83% rename from src/LibHac/Fs/HierarchicalIntegrityVerificationStorage.cs rename to src/LibHac/FsSystem/HierarchicalIntegrityVerificationStorage.cs index a81f438a..520436ea 100644 --- a/src/LibHac/Fs/HierarchicalIntegrityVerificationStorage.cs +++ b/src/LibHac/FsSystem/HierarchicalIntegrityVerificationStorage.cs @@ -3,8 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class HierarchicalIntegrityVerificationStorage : StorageBase { @@ -17,7 +18,8 @@ namespace LibHac.Fs /// public Validity[][] LevelValidities { get; } - private long _length; + private long Length { get; } + private bool LeaveOpen { get; } private IntegrityVerificationStorage[] IntegrityStorages { get; } @@ -33,8 +35,9 @@ namespace LibHac.Fs for (int i = 1; i < Levels.Length; i++) { var levelData = new IntegrityVerificationStorage(levelInfo[i], Levels[i - 1], integrityCheckLevel, leaveOpen); + levelData.GetSize(out long levelSize).ThrowIfFailure(); - int cacheCount = Math.Min((int)Util.DivideByRoundUp(levelData.GetSize(), levelInfo[i].BlockSize), 4); + int cacheCount = Math.Min((int)Util.DivideByRoundUp(levelSize, levelInfo[i].BlockSize), 4); Levels[i] = new CachedStorage(levelData, cacheCount, leaveOpen); LevelValidities[i - 1] = levelData.BlockValidities; @@ -42,9 +45,10 @@ namespace LibHac.Fs } DataLevel = Levels[Levels.Length - 1]; - _length = DataLevel.GetSize(); + DataLevel.GetSize(out long dataSize).ThrowIfFailure(); + Length = dataSize; - if (!leaveOpen) ToDispose.Add(DataLevel); + LeaveOpen = leaveOpen; } public HierarchicalIntegrityVerificationStorage(IvfcHeader header, IStorage masterHash, IStorage data, @@ -92,22 +96,42 @@ namespace LibHac.Fs return initInfo; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { - DataLevel.Read(destination, offset); + return DataLevel.Read(offset, destination); } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { - DataLevel.Write(source, offset); + return DataLevel.Write(offset, source); } - public override void Flush() + protected override Result FlushImpl() { - DataLevel.Flush(); + return DataLevel.Flush(); } - public override long GetSize() => _length; + protected override Result SetSizeImpl(long size) + { + return ResultFs.UnsupportedOperationInHierarchicalIvfcStorageSetSize.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!LeaveOpen) + { + DataLevel?.Dispose(); + } + } + } /// /// Checks the hashes of any unchecked blocks and returns the of the data. @@ -121,7 +145,7 @@ namespace LibHac.Fs IntegrityVerificationStorage storage = IntegrityStorages[IntegrityStorages.Length - 1]; long blockSize = storage.SectorSize; - int blockCount = (int)Util.DivideByRoundUp(_length, blockSize); + int blockCount = (int)Util.DivideByRoundUp(Length, blockSize); var buffer = new byte[blockSize]; var result = Validity.Valid; @@ -132,8 +156,10 @@ namespace LibHac.Fs { if (validities[i] == Validity.Unchecked) { - int toRead = (int)Math.Min(storage.GetSize() - blockSize * i, buffer.Length); - storage.Read(buffer.AsSpan(0, toRead), blockSize * i, IntegrityCheckLevel.IgnoreOnInvalid); + storage.GetSize(out long storageSize).ThrowIfFailure(); + int toRead = (int)Math.Min(storageSize - blockSize * i, buffer.Length); + + storage.Read(blockSize * i, buffer.AsSpan(0, toRead), IntegrityCheckLevel.IgnoreOnInvalid); } if (validities[i] == Validity.Invalid) diff --git a/src/LibHac/Fs/IndirectStorage.cs b/src/LibHac/FsSystem/IndirectStorage.cs similarity index 54% rename from src/LibHac/Fs/IndirectStorage.cs rename to src/LibHac/FsSystem/IndirectStorage.cs index 443e9c1a..c1387d8e 100644 --- a/src/LibHac/Fs/IndirectStorage.cs +++ b/src/LibHac/FsSystem/IndirectStorage.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class IndirectStorage : StorageBase { @@ -11,29 +12,30 @@ namespace LibHac.Fs private List Sources { get; } = new List(); private BucketTree BucketTree { get; } - private long _length; + private long Length { get; } + private bool LeaveOpen { get; } public IndirectStorage(IStorage bucketTreeData, bool leaveOpen, params IStorage[] sources) { Sources.AddRange(sources); - if (!leaveOpen) ToDispose.AddRange(sources); + LeaveOpen = leaveOpen; BucketTree = new BucketTree(bucketTreeData); RelocationEntries = BucketTree.GetEntryList(); RelocationOffsets = RelocationEntries.Select(x => x.Offset).ToList(); - _length = BucketTree.BucketOffsets.OffsetEnd; + Length = BucketTree.BucketOffsets.OffsetEnd; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { RelocationEntry entry = GetRelocationEntry(offset); if (entry.SourceIndex > Sources.Count) { - ThrowHelper.ThrowResult(ResultFs.InvalidIndirectStorageSource); + return ResultFs.InvalidIndirectStorageSource.Log(); } long inPos = offset; @@ -45,7 +47,9 @@ namespace LibHac.Fs long entryPos = inPos - entry.Offset; int bytesToRead = (int)Math.Min(entry.OffsetEnd - inPos, remaining); - Sources[entry.SourceIndex].Read(destination.Slice(outPos, bytesToRead), entry.SourceOffset + entryPos); + + Result rc = Sources[entry.SourceIndex].Read(entry.SourceOffset + entryPos, destination.Slice(outPos, bytesToRead)); + if (rc.IsFailure()) return rc; outPos += bytesToRead; inPos += bytesToRead; @@ -56,20 +60,43 @@ namespace LibHac.Fs entry = entry.Next; } } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationInIndirectStorageWrite); + return ResultFs.UnsupportedOperationInIndirectStorageSetSize.Log(); } - public override void Flush() { } - - public override long GetSize() => _length; - - public override void SetSize(long size) + protected override Result FlushImpl() { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationInIndirectStorageSetSize); + return Result.Success; + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.UnsupportedOperationInIndirectStorageSetSize.Log(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!LeaveOpen && Sources != null) + { + foreach (IStorage storage in Sources) + { + storage?.Dispose(); + } + } + } } private RelocationEntry GetRelocationEntry(long offset) diff --git a/src/LibHac/Fs/IntegrityVerificationStorage.cs b/src/LibHac/FsSystem/IntegrityVerificationStorage.cs similarity index 80% rename from src/LibHac/Fs/IntegrityVerificationStorage.cs rename to src/LibHac/FsSystem/IntegrityVerificationStorage.cs index e03db3df..6f10630b 100644 --- a/src/LibHac/Fs/IntegrityVerificationStorage.cs +++ b/src/LibHac/FsSystem/IntegrityVerificationStorage.cs @@ -2,9 +2,10 @@ using System.Buffers; using System.IO; using System.Security.Cryptography; -using LibHac.Fs.Save; +using LibHac.Fs; +using LibHac.FsSystem.Save; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class IntegrityVerificationStorage : SectorStorage { @@ -32,7 +33,7 @@ namespace LibHac.Fs BlockValidities = new Validity[SectorCount]; } - private void ReadImpl(Span destination, long offset, IntegrityCheckLevel integrityCheckLevel) + private Result ReadImpl(long offset, Span destination, IntegrityCheckLevel integrityCheckLevel) { int count = destination.Length; @@ -52,13 +53,13 @@ namespace LibHac.Fs if (Type != IntegrityStorageType.Save && !needsHashCheck) { - BaseStorage.Read(destination, offset); - return; + BaseStorage.Read(offset, destination); + return Result.Success; } Span hashBuffer = stackalloc byte[DigestSize]; long hashPos = blockIndex * DigestSize; - HashStorage.Read(hashBuffer, hashPos); + HashStorage.Read(hashPos, hashBuffer); if (Type == IntegrityStorageType.Save) { @@ -66,23 +67,23 @@ namespace LibHac.Fs { destination.Clear(); BlockValidities[blockIndex] = Validity.Valid; - return; + return Result.Success; } if (!needsHashCheck) { - BaseStorage.Read(destination, offset); - return; + BaseStorage.Read(offset, destination); + return Result.Success; } } byte[] dataBuffer = ArrayPool.Shared.Rent(SectorSize); try { - BaseStorage.Read(destination, offset); + BaseStorage.Read(offset, destination); destination.CopyTo(dataBuffer); - if (BlockValidities[blockIndex] != Validity.Unchecked) return; + if (BlockValidities[blockIndex] != Validity.Unchecked) return Result.Success; int bytesToHash = SectorSize; @@ -112,25 +113,30 @@ namespace LibHac.Fs { ArrayPool.Shared.Return(dataBuffer); } + + return Result.Success; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { - ReadImpl(destination, offset, IntegrityCheckLevel); + return ReadImpl(offset, destination, IntegrityCheckLevel); } - public void Read(Span destination, long offset, IntegrityCheckLevel integrityCheckLevel) + public Result Read(long offset, Span destination, IntegrityCheckLevel integrityCheckLevel) { - ValidateParameters(destination, offset); - ReadImpl(destination, offset, integrityCheckLevel); + // ValidateParameters(destination, offset); + return ReadImpl(offset, destination, integrityCheckLevel); } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { long blockIndex = offset / SectorSize; long hashPos = blockIndex * DigestSize; - int toWrite = (int)Math.Min(source.Length, GetSize() - offset); + Result rc = GetSize(out long storageSize); + if (rc.IsFailure()) return rc; + + int toWrite = (int)Math.Min(source.Length, storageSize - offset); byte[] dataBuffer = ArrayPool.Shared.Rent(SectorSize); try @@ -143,15 +149,17 @@ namespace LibHac.Fs Array.Clear(hash, 0, DigestSize); } - BaseStorage.Write(source, offset); + BaseStorage.Write(offset, source); - HashStorage.Write(hash, hashPos); + HashStorage.Write(hashPos, hash); BlockValidities[blockIndex] = Validity.Unchecked; } finally { ArrayPool.Shared.Return(dataBuffer); } + + return Result.Success; } private byte[] DoHash(byte[] buffer, int offset, int count) @@ -180,10 +188,12 @@ namespace LibHac.Fs } } - public override void Flush() + protected override Result FlushImpl() { - HashStorage.Flush(); - base.Flush(); + Result rc = HashStorage.Flush(); + if (rc.IsFailure()) return rc; + + return base.FlushImpl(); } public void FsTrim() @@ -195,7 +205,7 @@ namespace LibHac.Fs for (int i = 0; i < SectorCount; i++) { long hashPos = i * DigestSize; - HashStorage.Read(digest, hashPos); + HashStorage.Read(hashPos, digest).ThrowIfFailure(); if (!Util.IsEmpty(digest)) continue; diff --git a/src/LibHac/FsSystem/LayeredFileSystem.cs b/src/LibHac/FsSystem/LayeredFileSystem.cs new file mode 100644 index 00000000..c48a33cb --- /dev/null +++ b/src/LibHac/FsSystem/LayeredFileSystem.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class LayeredFileSystem : FileSystemBase + { + private List Sources { get; } = new List(); + + public LayeredFileSystem(IList sourceFileSystems) + { + Sources.AddRange(sourceFileSystems); + } + + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) + { + directory = default; + path = PathTools.Normalize(path); + + var dirs = new List(); + + foreach (IFileSystem fs in Sources) + { + Result rc = fs.GetEntryType(out DirectoryEntryType entryType, path); + if (rc.IsFailure()) return rc; + + if (entryType == DirectoryEntryType.File && dirs.Count == 0) + { + ThrowHelper.ThrowResult(ResultFs.PathNotFound); + } + + if (entryType == DirectoryEntryType.Directory) + { + rc = fs.OpenDirectory(out IDirectory subDirectory, path, mode); + if (rc.IsFailure()) return rc; + + dirs.Add(subDirectory); + } + } + + directory = new LayeredFileSystemDirectory(dirs); + return Result.Success; + } + + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) + { + file = default; + path = PathTools.Normalize(path); + + foreach (IFileSystem fs in Sources) + { + Result rc = fs.GetEntryType(out DirectoryEntryType type, path); + if (rc.IsFailure()) return rc; + + if (type == DirectoryEntryType.File) + { + return fs.OpenFile(out file, path, mode); + } + + if (type == DirectoryEntryType.Directory) + { + return ResultFs.PathNotFound.Log(); + } + } + + return ResultFs.PathNotFound.Log(); + } + + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) + { + path = PathTools.Normalize(path); + + foreach (IFileSystem fs in Sources) + { + Result getEntryResult = fs.GetEntryType(out DirectoryEntryType type, path); + + if (getEntryResult.IsSuccess() && type != DirectoryEntryType.NotFound) + { + entryType = type; + return Result.Success; + } + } + + entryType = DirectoryEntryType.NotFound; + return ResultFs.PathNotFound.Log(); + } + + protected override Result GetFileTimeStampRawImpl(out FileTimeStampRaw timeStamp, string path) + { + path = PathTools.Normalize(path); + + foreach (IFileSystem fs in Sources) + { + Result getEntryResult = fs.GetEntryType(out DirectoryEntryType type, path); + + if (getEntryResult.IsSuccess() && type != DirectoryEntryType.NotFound) + { + return fs.GetFileTimeStampRaw(out timeStamp, path); + } + } + + timeStamp = default; + return ResultFs.PathNotFound.Log(); + } + + protected override Result QueryEntryImpl(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, string path) + { + path = PathTools.Normalize(path); + + foreach (IFileSystem fs in Sources) + { + Result getEntryResult = fs.GetEntryType(out DirectoryEntryType type, path); + + if (getEntryResult.IsSuccess() && type != DirectoryEntryType.NotFound) + { + return fs.QueryEntry(outBuffer, inBuffer, queryId, path); + } + } + + return ResultFs.PathNotFound.Log(); + } + + protected override Result CommitImpl() + { + return Result.Success; + } + + protected override Result CreateDirectoryImpl(string path) => ResultFs.UnsupportedOperation.Log(); + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) => ResultFs.UnsupportedOperation.Log(); + protected override Result DeleteDirectoryImpl(string path) => ResultFs.UnsupportedOperation.Log(); + protected override Result DeleteDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperation.Log(); + protected override Result CleanDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperation.Log(); + protected override Result DeleteFileImpl(string path) => ResultFs.UnsupportedOperation.Log(); + protected override Result RenameDirectoryImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperation.Log(); + protected override Result RenameFileImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperation.Log(); + } +} diff --git a/src/LibHac/FsSystem/LayeredFileSystemDirectory.cs b/src/LibHac/FsSystem/LayeredFileSystemDirectory.cs new file mode 100644 index 00000000..244beeed --- /dev/null +++ b/src/LibHac/FsSystem/LayeredFileSystemDirectory.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class LayeredFileSystemDirectory : IDirectory + { + private List Sources { get; } + + public LayeredFileSystemDirectory(List sources) + { + Sources = sources; + } + + // Todo: Don't return duplicate entries + public Result Read(out long entriesRead, Span entryBuffer) + { + entriesRead = 0; + int entryIndex = 0; + + for (int i = 0; i < Sources.Count && entryIndex < entryBuffer.Length; i++) + { + Result rs = Sources[i].Read(out long subEntriesRead, entryBuffer.Slice(entryIndex)); + if (rs.IsFailure()) return rs; + + entryIndex += (int)subEntriesRead; + } + + entriesRead = entryIndex; + return Result.Success; + } + + // Todo: Don't count duplicate entries + public Result GetEntryCount(out long entryCount) + { + entryCount = 0; + long totalEntryCount = 0; + + foreach (IDirectory dir in Sources) + { + Result rc = dir.GetEntryCount(out long subEntryCount); + if (rc.IsFailure()) return rc; + + totalEntryCount += subEntryCount; + } + + entryCount = totalEntryCount; + return Result.Success; + } + } +} diff --git a/src/LibHac/FsSystem/LocalDirectory.cs b/src/LibHac/FsSystem/LocalDirectory.cs new file mode 100644 index 00000000..f7cde12b --- /dev/null +++ b/src/LibHac/FsSystem/LocalDirectory.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class LocalDirectory : IDirectory + { + private OpenDirectoryMode Mode { get; } + private DirectoryInfo DirInfo { get; } + private IEnumerator EntryEnumerator { get; } + + public LocalDirectory(IEnumerator entryEnumerator, DirectoryInfo dirInfo, + OpenDirectoryMode mode) + { + EntryEnumerator = entryEnumerator; + DirInfo = dirInfo; + Mode = mode; + } + + public Result Read(out long entriesRead, Span entryBuffer) + { + int i = 0; + + while (i < entryBuffer.Length && EntryEnumerator.MoveNext()) + { + FileSystemInfo localEntry = EntryEnumerator.Current; + if (localEntry == null) break; + + bool isDir = localEntry.Attributes.HasFlag(FileAttributes.Directory); + + if (!CanReturnEntry(isDir, Mode)) continue; + + ReadOnlySpan name = Util.GetUtf8Bytes(localEntry.Name); + DirectoryEntryType type = isDir ? DirectoryEntryType.Directory : DirectoryEntryType.File; + long length = isDir ? 0 : ((FileInfo)localEntry).Length; + + StringUtils.Copy(entryBuffer[i].Name, name); + entryBuffer[i].Name[PathTools.MaxPathLength] = 0; + + entryBuffer[i].Attributes = localEntry.Attributes.ToNxAttributes(); + entryBuffer[i].Type = type; + entryBuffer[i].Size = length; + + i++; + } + + entriesRead = i; + return Result.Success; + } + + public Result GetEntryCount(out long entryCount) + { + int count = 0; + + foreach (FileSystemInfo entry in DirInfo.EnumerateFileSystemInfos()) + { + bool isDir = (entry.Attributes & FileAttributes.Directory) != 0; + + if (CanReturnEntry(isDir, Mode)) count++; + } + + entryCount = count; + return Result.Success; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanReturnEntry(bool isDir, OpenDirectoryMode mode) + { + return isDir && (mode & OpenDirectoryMode.Directory) != 0 || + !isDir && (mode & OpenDirectoryMode.File) != 0; + } + } +} diff --git a/src/LibHac/FsSystem/LocalFile.cs b/src/LibHac/FsSystem/LocalFile.cs new file mode 100644 index 00000000..6e922cc0 --- /dev/null +++ b/src/LibHac/FsSystem/LocalFile.cs @@ -0,0 +1,97 @@ +using System; +using System.IO; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class LocalFile : FileBase + { + private FileStream Stream { get; } + private StreamFile File { get; } + private OpenMode Mode { get; } + + public LocalFile(string path, OpenMode mode) + { + LocalFileSystem.OpenFileInternal(out FileStream stream, path, mode).ThrowIfFailure(); + + Mode = mode; + Stream = stream; + File = new StreamFile(Stream, mode); + } + + public LocalFile(FileStream stream, OpenMode mode) + { + Mode = mode; + Stream = stream; + File = new StreamFile(Stream, mode); + } + + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) + { + bytesRead = 0; + + Result rc = ValidateReadParams(out long toRead, offset, destination.Length, Mode); + if (rc.IsFailure()) return rc; + + return File.Read(out bytesRead, offset, destination.Slice(0, (int)toRead), options); + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) + { + Result rc = ValidateWriteParams(offset, source.Length, Mode, out _); + if (rc.IsFailure()) return rc; + + return File.Write(offset, source, options); + } + + protected override Result FlushImpl() + { + try + { + return File.Flush(); + } + catch (Exception ex) when (ex.HResult < 0) + { + return ResultFs.UnexpectedErrorInHostFileFlush.Log(); + } + } + + protected override Result GetSizeImpl(out long size) + { + try + { + return File.GetSize(out size); + } + catch (Exception ex) when (ex.HResult < 0) + { + size = default; + return ResultFs.UnexpectedErrorInHostFileGetSize.Log(); + } + } + + protected override Result SetSizeImpl(long size) + { + try + { + File.SetSize(size); + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + File?.Dispose(); + } + + Stream?.Dispose(); + } + } +} diff --git a/src/LibHac/FsSystem/LocalFileSystem.cs b/src/LibHac/FsSystem/LocalFileSystem.cs new file mode 100644 index 00000000..69425b06 --- /dev/null +++ b/src/LibHac/FsSystem/LocalFileSystem.cs @@ -0,0 +1,556 @@ +using System; +using System.Collections.Generic; +using System.IO; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class LocalFileSystem : IAttributeFileSystem + { + private string BasePath { get; } + + /// + /// Opens a directory on local storage as an . + /// The directory will be created if it does not exist. + /// + /// The path that will be the root of the . + public LocalFileSystem(string basePath) + { + BasePath = Path.GetFullPath(basePath); + + if (!Directory.Exists(BasePath)) + { + Directory.CreateDirectory(BasePath); + } + } + + internal string ResolveLocalPath(string path) + { + return PathTools.Combine(BasePath, path); + } + + public Result GetFileAttributes(string path, out NxFileAttributes attributes) + { + attributes = default; + + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetFileInfo(out FileInfo info, localPath); + if (rc.IsFailure()) return rc; + + if (info.Attributes == (FileAttributes)(-1)) + { + attributes = default; + return ResultFs.PathNotFound.Log(); + } + + attributes = info.Attributes.ToNxAttributes(); + return Result.Success; + } + + public Result SetFileAttributes(string path, NxFileAttributes attributes) + { + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetFileInfo(out FileInfo info, localPath); + if (rc.IsFailure()) return rc; + + if (info.Attributes == (FileAttributes)(-1)) + { + return ResultFs.PathNotFound.Log(); + } + + FileAttributes attributesOld = info.Attributes; + FileAttributes attributesNew = attributesOld.ApplyNxAttributes(attributes); + + try + { + info.Attributes = attributesNew; + } + catch (IOException) + { + return ResultFs.PathNotFound.Log(); + } + + return Result.Success; + } + + public Result GetFileSize(out long fileSize, string path) + { + fileSize = default; + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetFileInfo(out FileInfo info, localPath); + if (rc.IsFailure()) return rc; + + return GetSizeInternal(out fileSize, info); + } + + public Result CreateDirectory(string path) + { + return CreateDirectory(path, NxFileAttributes.None); + } + + public Result CreateDirectory(string path, NxFileAttributes archiveAttribute) + { + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetDirInfo(out DirectoryInfo dir, localPath); + if (rc.IsFailure()) return rc; + + if (dir.Exists) + { + return ResultFs.PathAlreadyExists.Log(); + } + + if (dir.Parent?.Exists != true) + { + return ResultFs.PathNotFound.Log(); + } + + return CreateDirInternal(dir, archiveAttribute); + } + + public Result CreateFile(string path, long size, CreateFileOptions options) + { + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetFileInfo(out FileInfo file, localPath); + if (rc.IsFailure()) return rc; + + if (file.Exists) + { + return ResultFs.PathAlreadyExists.Log(); + } + + if (file.Directory?.Exists != true) + { + return ResultFs.PathNotFound.Log(); + } + + rc = CreateFileInternal(out FileStream stream, file); + + using (stream) + { + if (rc.IsFailure()) return rc; + + return SetStreamLengthInternal(stream, size); + } + } + + public Result DeleteDirectory(string path) + { + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetDirInfo(out DirectoryInfo dir, localPath); + if (rc.IsFailure()) return rc; + + return DeleteDirectoryInternal(dir, false); + } + + public Result DeleteDirectoryRecursively(string path) + { + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetDirInfo(out DirectoryInfo dir, localPath); + if (rc.IsFailure()) return rc; + + return DeleteDirectoryInternal(dir, true); + } + + public Result CleanDirectoryRecursively(string path) + { + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + foreach (string file in Directory.EnumerateFiles(localPath)) + { + Result rc = GetFileInfo(out FileInfo fileInfo, file); + if (rc.IsFailure()) return rc; + + rc = DeleteFileInternal(fileInfo); + if (rc.IsFailure()) return rc; + } + + foreach (string dir in Directory.EnumerateDirectories(localPath)) + { + Result rc = GetDirInfo(out DirectoryInfo dirInfo, dir); + if (rc.IsFailure()) return rc; + + rc = DeleteDirectoryInternal(dirInfo, true); + if (rc.IsFailure()) return rc; + } + + return Result.Success; + } + + public Result DeleteFile(string path) + { + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetFileInfo(out FileInfo file, localPath); + if (rc.IsFailure()) return rc; + + return DeleteFileInternal(file); + } + + public Result OpenDirectory(out IDirectory directory, string path, OpenDirectoryMode mode) + { + directory = default; + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetDirInfo(out DirectoryInfo dirInfo, localPath); + if (rc.IsFailure()) return rc; + + if (!dirInfo.Attributes.HasFlag(FileAttributes.Directory)) + { + return ResultFs.PathNotFound.Log(); + } + + try + { + IEnumerator entryEnumerator = dirInfo.EnumerateFileSystemInfos().GetEnumerator(); + + directory = new LocalDirectory(entryEnumerator, dirInfo, mode); + return Result.Success; + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + } + + public Result OpenFile(out IFile file, string path, OpenMode mode) + { + file = default; + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetEntryType(out DirectoryEntryType entryType, path); + if (rc.IsFailure()) return rc; + + if (entryType == DirectoryEntryType.Directory) + { + return ResultFs.PathNotFound.Log(); + } + + rc = OpenFileInternal(out FileStream fileStream, localPath, mode); + if (rc.IsFailure()) return rc; + + file = new LocalFile(fileStream, mode); + return Result.Success; + } + + public Result RenameDirectory(string oldPath, string newPath) + { + oldPath = PathTools.Normalize(oldPath); + newPath = PathTools.Normalize(newPath); + + // Official FS behavior is to do nothing in this case + if (oldPath == newPath) return Result.Success; + + // FS does the subpath check before verifying the path exists + if (PathTools.IsSubPath(oldPath.AsSpan(), newPath.AsSpan())) + { + ThrowHelper.ThrowResult(ResultFs.DestinationIsSubPathOfSource); + } + + Result rc = GetDirInfo(out DirectoryInfo srcDir, ResolveLocalPath(oldPath)); + if (rc.IsFailure()) return rc; + + rc = GetDirInfo(out DirectoryInfo dstDir, ResolveLocalPath(newPath)); + if (rc.IsFailure()) return rc; + + return RenameDirInternal(srcDir, dstDir); + } + + public Result RenameFile(string oldPath, string newPath) + { + string srcLocalPath = ResolveLocalPath(PathTools.Normalize(oldPath)); + string dstLocalPath = ResolveLocalPath(PathTools.Normalize(newPath)); + + // Official FS behavior is to do nothing in this case + if (srcLocalPath == dstLocalPath) return Result.Success; + + Result rc = GetFileInfo(out FileInfo srcFile, srcLocalPath); + if (rc.IsFailure()) return rc; + + rc = GetFileInfo(out FileInfo dstFile, dstLocalPath); + if (rc.IsFailure()) return rc; + + return RenameFileInternal(srcFile, dstFile); + } + + public Result GetEntryType(out DirectoryEntryType entryType, string path) + { + entryType = default; + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetDirInfo(out DirectoryInfo dir, localPath); + if (rc.IsFailure()) return rc; + + if (dir.Exists) + { + entryType = DirectoryEntryType.Directory; + return Result.Success; + } + + rc = GetFileInfo(out FileInfo file, localPath); + if (rc.IsFailure()) return rc; + + if (file.Exists) + { + entryType = DirectoryEntryType.File; + return Result.Success; + } + + entryType = DirectoryEntryType.NotFound; + return ResultFs.PathNotFound.Log(); + } + + public Result GetFileTimeStampRaw(out FileTimeStampRaw timeStamp, string path) + { + timeStamp = default; + string localPath = ResolveLocalPath(PathTools.Normalize(path)); + + Result rc = GetFileInfo(out FileInfo file, localPath); + if (rc.IsFailure()) return rc; + + if (!file.Exists) return ResultFs.PathNotFound.Log(); + + timeStamp.Created = new DateTimeOffset(File.GetCreationTime(localPath)).ToUnixTimeSeconds(); + timeStamp.Accessed = new DateTimeOffset(File.GetLastAccessTime(localPath)).ToUnixTimeSeconds(); + timeStamp.Modified = new DateTimeOffset(File.GetLastWriteTime(localPath)).ToUnixTimeSeconds(); + + return Result.Success; + } + + public Result GetFreeSpaceSize(out long freeSpace, string path) + { + freeSpace = new DriveInfo(BasePath).AvailableFreeSpace; + return Result.Success; + } + + public Result GetTotalSpaceSize(out long totalSpace, string path) + { + totalSpace = new DriveInfo(BasePath).TotalSize; + return Result.Success; + } + + public Result Commit() + { + return Result.Success; + } + + public Result QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, string path) + { + return ResultFs.UnsupportedOperation.Log(); + } + + internal static FileAccess GetFileAccess(OpenMode mode) + { + // FileAccess and OpenMode have the same flags + return (FileAccess)(mode & OpenMode.ReadWrite); + } + + internal static FileShare GetFileShare(OpenMode mode) + { + return mode.HasFlag(OpenMode.Write) ? FileShare.Read : FileShare.ReadWrite; + } + + internal static Result OpenFileInternal(out FileStream stream, string path, OpenMode mode) + { + try + { + stream = new FileStream(path, FileMode.Open, GetFileAccess(mode), GetFileShare(mode)); + return Result.Success; + } + catch (Exception ex) when (ex.HResult < 0) + { + stream = default; + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + } + + private static Result GetSizeInternal(out long fileSize, FileInfo file) + { + try + { + fileSize = file.Length; + return Result.Success; + } + catch (Exception ex) when (ex.HResult < 0) + { + fileSize = default; + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + } + + private static Result CreateFileInternal(out FileStream file, FileInfo fileInfo) + { + file = default; + + try + { + file = new FileStream(fileInfo.FullName, FileMode.CreateNew, FileAccess.ReadWrite); + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + + return Result.Success; + } + + private static Result SetStreamLengthInternal(Stream stream, long size) + { + try + { + stream.SetLength(size); + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + + return Result.Success; + } + + private static Result DeleteDirectoryInternal(DirectoryInfo dir, bool recursive) + { + if (!dir.Exists) return ResultFs.PathNotFound.Log(); + + try + { + dir.Delete(recursive); + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + + EnsureDeleted(dir); + + return Result.Success; + } + + private static Result DeleteFileInternal(FileInfo file) + { + if (!file.Exists) return ResultFs.PathNotFound.Log(); + + try + { + file.Delete(); + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + + EnsureDeleted(file); + + return Result.Success; + } + + private static Result CreateDirInternal(DirectoryInfo dir, NxFileAttributes attributes) + { + try + { + dir.Create(); + dir.Refresh(); + + if (attributes.HasFlag(NxFileAttributes.Archive)) + { + dir.Attributes |= FileAttributes.Archive; + } + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + + return Result.Success; + } + + private static Result RenameDirInternal(DirectoryInfo source, DirectoryInfo dest) + { + if (!source.Exists) return ResultFs.PathNotFound.Log(); + if (dest.Exists) return ResultFs.PathAlreadyExists.Log(); + + try + { + source.MoveTo(dest.FullName); + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + + return Result.Success; + } + + private static Result RenameFileInternal(FileInfo source, FileInfo dest) + { + if (!source.Exists) return ResultFs.PathNotFound.Log(); + if (dest.Exists) return ResultFs.PathAlreadyExists.Log(); + + try + { + source.MoveTo(dest.FullName); + } + catch (Exception ex) when (ex.HResult < 0) + { + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + + return Result.Success; + } + + // GetFileInfo and GetDirInfo detect invalid paths + private static Result GetFileInfo(out FileInfo fileInfo, string path) + { + try + { + fileInfo = new FileInfo(path); + return Result.Success; + } + catch (Exception ex) when (ex.HResult < 0) + { + fileInfo = default; + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + } + + private static Result GetDirInfo(out DirectoryInfo directoryInfo, string path) + { + try + { + directoryInfo = new DirectoryInfo(path); + return Result.Success; + } + catch (Exception ex) when (ex.HResult < 0) + { + directoryInfo = default; + return HResult.HResultToHorizonResult(ex.HResult).Log(); + } + } + + // Delete operations on IFileSystem should be synchronous + // DeleteFile and RemoveDirectory only mark the file for deletion, so we need + // to poll the filesystem until it's actually gone + private static void EnsureDeleted(FileSystemInfo entry) + { + int tries = 0; + + do + { + entry.Refresh(); + tries++; + + if (tries > 1000) + { + throw new IOException($"Unable to delete file {entry.FullName}"); + } + } while (entry.Exists); + } + } +} diff --git a/src/LibHac/FsSystem/LocalStorage.cs b/src/LibHac/FsSystem/LocalStorage.cs new file mode 100644 index 00000000..ebc47f58 --- /dev/null +++ b/src/LibHac/FsSystem/LocalStorage.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class LocalStorage : StorageBase + { + private string Path { get; } + private FileStream Stream { get; } + private StreamStorage Storage { get; } + + public LocalStorage(string path, FileAccess access) : this(path, access, FileMode.Open) { } + + public LocalStorage(string path, FileAccess access, FileMode mode) + { + Path = path; + Stream = new FileStream(Path, mode, access); + Storage = new StreamStorage(Stream, false); + } + + protected override Result ReadImpl(long offset, Span destination) + { + return Storage.Read(offset, destination); + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source) + { + return Storage.Write(offset, source); + } + + protected override Result FlushImpl() + { + return Storage.Flush(); + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.NotImplemented.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + return Storage.GetSize(out size); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Storage?.Dispose(); + Stream?.Dispose(); + } + } + } +} diff --git a/src/LibHac/Fs/MemoryStorage.cs b/src/LibHac/FsSystem/MemoryStorage.cs similarity index 78% rename from src/LibHac/Fs/MemoryStorage.cs rename to src/LibHac/FsSystem/MemoryStorage.cs index fb88a181..b655b885 100644 --- a/src/LibHac/Fs/MemoryStorage.cs +++ b/src/LibHac/FsSystem/MemoryStorage.cs @@ -1,6 +1,7 @@ using System; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class MemoryStorage : StorageBase { @@ -9,7 +10,7 @@ namespace LibHac.Fs private int _length; private int _capacity; private bool _isExpandable; - + public MemoryStorage() : this(0) { } public MemoryStorage(int capacity) @@ -18,7 +19,6 @@ namespace LibHac.Fs _capacity = capacity; _isExpandable = true; - CanAutoExpand = true; _buffer = new byte[capacity]; } @@ -38,13 +38,21 @@ namespace LibHac.Fs _isExpandable = false; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { + if (!IsRangeValid(offset, destination.Length, _length)) + return ResultFs.ValueOutOfRange.Log(); + _buffer.AsSpan((int)(_start + offset), destination.Length).CopyTo(destination); + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { + if (!IsRangeValid(offset, source.Length, _length)) + return ResultFs.ValueOutOfRange.Log(); + long requiredCapacity = _start + offset + source.Length; if (requiredCapacity > _length) @@ -54,6 +62,8 @@ namespace LibHac.Fs } source.CopyTo(_buffer.AsSpan((int)(_start + offset), source.Length)); + + return Result.Success; } public byte[] ToArray() @@ -92,13 +102,17 @@ namespace LibHac.Fs } } - public override void Flush() { } + protected override Result FlushImpl() => Result.Success; - public override long GetSize() => _length; - - public override void SetSize(long size) + protected override Result GetSizeImpl(out long size) { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationInMemoryStorageSetSize); + size = _length; + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.UnsupportedOperationInMemoryStorageSetSize.Log(); } } } diff --git a/src/LibHac/Fs/Messages.cs b/src/LibHac/FsSystem/Messages.cs similarity index 95% rename from src/LibHac/Fs/Messages.cs rename to src/LibHac/FsSystem/Messages.cs index c9835942..63b58cae 100644 --- a/src/LibHac/Fs/Messages.cs +++ b/src/LibHac/FsSystem/Messages.cs @@ -1,4 +1,4 @@ -namespace LibHac.Fs +namespace LibHac.FsSystem { internal static class Messages { diff --git a/src/LibHac/Fs/NcaUtils/Nca.cs b/src/LibHac/FsSystem/NcaUtils/Nca.cs similarity index 91% rename from src/LibHac/Fs/NcaUtils/Nca.cs rename to src/LibHac/FsSystem/NcaUtils/Nca.cs index bad42048..df7af1e8 100644 --- a/src/LibHac/Fs/NcaUtils/Nca.cs +++ b/src/LibHac/FsSystem/NcaUtils/Nca.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using LibHac.Fs.RomFs; +using LibHac.Fs; +using LibHac.FsSystem.RomFs; +using LibHac.Spl; -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { public class Nca { @@ -46,9 +48,9 @@ namespace LibHac.Fs.NcaUtils int keyRevision = Util.GetMasterKeyRevision(Header.KeyGeneration); byte[] titleKek = Keyset.TitleKeks[keyRevision]; - if (!Keyset.TitleKeys.TryGetValue(Header.RightsId.ToArray(), out byte[] encryptedKey)) + if (Keyset.ExternalKeySet.Get(new RightsId(Header.RightsId), out AccessKey accessKey).IsFailure()) { - throw new MissingKeyException("Missing NCA title key.", Header.RightsId.ToHexString(), KeyType.Title); + throw new MissingKeyException("Missing NCA title key.", Header.RightsId.ToString(), KeyType.Title); } if (titleKek.IsEmpty()) @@ -57,6 +59,7 @@ namespace LibHac.Fs.NcaUtils throw new MissingKeyException("Unable to decrypt title key.", keyName, KeyType.Common); } + byte[] encryptedKey = accessKey.Value.ToArray(); var decryptedKey = new byte[Crypto.Aes128Size]; Crypto.DecryptEcb(titleKek, encryptedKey, decryptedKey, Crypto.Aes128Size); @@ -88,7 +91,7 @@ namespace LibHac.Fs.NcaUtils if (Header.HasRightsId) { - return Keyset.TitleKeys.ContainsKey(Header.RightsId.ToArray()) && + return Keyset.ExternalKeySet.Contains(new RightsId(Header.RightsId)) && !Keyset.TitleKeks[keyRevision].IsEmpty(); } @@ -117,10 +120,12 @@ namespace LibHac.Fs.NcaUtils long offset = Header.GetSectionStartOffset(index); long size = Header.GetSectionSize(index); - if (!Util.IsSubRange(offset, size, BaseStorage.GetSize())) + BaseStorage.GetSize(out long baseSize).ThrowIfFailure(); + + if (!Util.IsSubRange(offset, size, baseSize)) { throw new InvalidDataException( - $"Section offset (0x{offset:x}) and length (0x{size:x}) fall outside the total NCA length (0x{BaseStorage.GetSize():x})."); + $"Section offset (0x{offset:x}) and length (0x{size:x}) fall outside the total NCA length (0x{baseSize:x})."); } return BaseStorage.Slice(offset, size); @@ -336,7 +341,7 @@ namespace LibHac.Fs.NcaUtils return GetSectionIndexFromType(type, Header.ContentType); } - public static int GetSectionIndexFromType(NcaSectionType type, ContentType contentType) + public static int GetSectionIndexFromType(NcaSectionType type, NcaContentType contentType) { if (!TryGetSectionIndexFromType(type, contentType, out int index)) { @@ -346,17 +351,17 @@ namespace LibHac.Fs.NcaUtils return index; } - public static bool TryGetSectionIndexFromType(NcaSectionType type, ContentType contentType, out int index) + public static bool TryGetSectionIndexFromType(NcaSectionType type, NcaContentType contentType, out int index) { switch (type) { - case NcaSectionType.Code when contentType == ContentType.Program: + case NcaSectionType.Code when contentType == NcaContentType.Program: index = 0; return true; - case NcaSectionType.Data when contentType == ContentType.Program: + case NcaSectionType.Data when contentType == NcaContentType.Program: index = 1; return true; - case NcaSectionType.Logo when contentType == ContentType.Program: + case NcaSectionType.Logo when contentType == NcaContentType.Program: index = 2; return true; case NcaSectionType.Data: @@ -368,7 +373,7 @@ namespace LibHac.Fs.NcaUtils } } - public static NcaSectionType GetSectionTypeFromIndex(int index, ContentType contentType) + public static NcaSectionType GetSectionTypeFromIndex(int index, NcaContentType contentType) { if (!TryGetSectionTypeFromIndex(index, contentType, out NcaSectionType type)) { @@ -378,17 +383,17 @@ namespace LibHac.Fs.NcaUtils return type; } - public static bool TryGetSectionTypeFromIndex(int index, ContentType contentType, out NcaSectionType type) + public static bool TryGetSectionTypeFromIndex(int index, NcaContentType contentType, out NcaSectionType type) { switch (index) { - case 0 when contentType == ContentType.Program: + case 0 when contentType == NcaContentType.Program: type = NcaSectionType.Code; return true; - case 1 when contentType == ContentType.Program: + case 1 when contentType == NcaContentType.Program: type = NcaSectionType.Data; return true; - case 2 when contentType == ContentType.Program: + case 2 when contentType == NcaContentType.Program: type = NcaSectionType.Logo; return true; case 0: @@ -492,7 +497,7 @@ namespace LibHac.Fs.NcaUtils private int ReadHeaderVersion(IStorage header) { Span buf = stackalloc byte[1]; - header.Read(buf, 0x203); + header.Read(0x203, buf).Log(); return buf[0] - '0'; } @@ -516,7 +521,7 @@ namespace LibHac.Fs.NcaUtils return Header.VerifySignature1(Keyset.NcaHdrFixedKeyModulus); } - internal void GenerateAesCounter(int sectionIndex, CnmtContentType type, int minorVersion) + internal void GenerateAesCounter(int sectionIndex, Ncm.ContentType type, int minorVersion) { int counterType; int counterVersion; @@ -527,14 +532,14 @@ namespace LibHac.Fs.NcaUtils switch (type) { - case CnmtContentType.Program: + case Ncm.ContentType.Program: counterType = sectionIndex + 1; break; - case CnmtContentType.HtmlDocument: - counterType = (int)CnmtContentType.HtmlDocument; + case Ncm.ContentType.HtmlDocument: + counterType = (int)Ncm.ContentType.HtmlDocument; break; - case CnmtContentType.LegalInformation: - counterType = (int)CnmtContentType.LegalInformation; + case Ncm.ContentType.LegalInformation: + counterType = (int)Ncm.ContentType.LegalInformation; break; default: counterType = 0; @@ -545,11 +550,11 @@ namespace LibHac.Fs.NcaUtils // Haven't checked delta fragment NCAs switch (Header.ContentType) { - case ContentType.Program: - case ContentType.Manual: + case NcaContentType.Program: + case NcaContentType.Manual: counterVersion = Math.Max(minorVersion - 1, 0); break; - case ContentType.PublicData: + case NcaContentType.PublicData: counterVersion = minorVersion << 16; break; default: diff --git a/src/LibHac/Fs/NcaUtils/NcaExtensions.cs b/src/LibHac/FsSystem/NcaUtils/NcaExtensions.cs similarity index 97% rename from src/LibHac/Fs/NcaUtils/NcaExtensions.cs rename to src/LibHac/FsSystem/NcaUtils/NcaExtensions.cs index 42f8bb0e..b9ddfc7f 100644 --- a/src/LibHac/Fs/NcaUtils/NcaExtensions.cs +++ b/src/LibHac/FsSystem/NcaUtils/NcaExtensions.cs @@ -1,8 +1,9 @@ using System; using System.Buffers.Binary; using System.Diagnostics; +using LibHac.Fs; -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { public static class NcaExtensions { @@ -95,7 +96,7 @@ namespace LibHac.Fs.NcaUtils IStorage storage = nca.OpenRawStorage(index); var data = new byte[size]; - storage.Read(data, offset); + storage.Read(offset, data).ThrowIfFailure(); byte[] actualHash = Crypto.ComputeSha256(data, 0, data.Length); @@ -116,7 +117,7 @@ namespace LibHac.Fs.NcaUtils IStorage decryptedStorage = nca.OpenRawStorage(index); Span buffer = stackalloc byte[sizeof(long)]; - decryptedStorage.Read(buffer, header.EncryptionTreeOffset + 8); + decryptedStorage.Read(header.EncryptionTreeOffset + 8, buffer).ThrowIfFailure(); long readDataSize = BinaryPrimitives.ReadInt64LittleEndian(buffer); if (header.EncryptionTreeOffset != readDataSize) return Validity.Invalid; diff --git a/src/LibHac/Fs/NcaUtils/NcaFsHeader.cs b/src/LibHac/FsSystem/NcaUtils/NcaFsHeader.cs similarity index 98% rename from src/LibHac/Fs/NcaUtils/NcaFsHeader.cs rename to src/LibHac/FsSystem/NcaUtils/NcaFsHeader.cs index e9dd9f52..a09bb783 100644 --- a/src/LibHac/Fs/NcaUtils/NcaFsHeader.cs +++ b/src/LibHac/FsSystem/NcaUtils/NcaFsHeader.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; // ReSharper disable ImpureMethodCallOnReadonlyValueField -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { public struct NcaFsHeader { diff --git a/src/LibHac/Fs/NcaUtils/NcaFsIntegrityInfoIvfc.cs b/src/LibHac/FsSystem/NcaUtils/NcaFsIntegrityInfoIvfc.cs similarity index 98% rename from src/LibHac/Fs/NcaUtils/NcaFsIntegrityInfoIvfc.cs rename to src/LibHac/FsSystem/NcaUtils/NcaFsIntegrityInfoIvfc.cs index 5f42be72..5e290c86 100644 --- a/src/LibHac/Fs/NcaUtils/NcaFsIntegrityInfoIvfc.cs +++ b/src/LibHac/FsSystem/NcaUtils/NcaFsIntegrityInfoIvfc.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { public struct NcaFsIntegrityInfoIvfc { diff --git a/src/LibHac/Fs/NcaUtils/NcaFsIntegrityInfoSha256.cs b/src/LibHac/FsSystem/NcaUtils/NcaFsIntegrityInfoSha256.cs similarity index 98% rename from src/LibHac/Fs/NcaUtils/NcaFsIntegrityInfoSha256.cs rename to src/LibHac/FsSystem/NcaUtils/NcaFsIntegrityInfoSha256.cs index 785f339d..7a8abd4d 100644 --- a/src/LibHac/Fs/NcaUtils/NcaFsIntegrityInfoSha256.cs +++ b/src/LibHac/FsSystem/NcaUtils/NcaFsIntegrityInfoSha256.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { public struct NcaFsIntegrityInfoSha256 { diff --git a/src/LibHac/Fs/NcaUtils/NcaFsPatchInfo.cs b/src/LibHac/FsSystem/NcaUtils/NcaFsPatchInfo.cs similarity index 97% rename from src/LibHac/Fs/NcaUtils/NcaFsPatchInfo.cs rename to src/LibHac/FsSystem/NcaUtils/NcaFsPatchInfo.cs index 5e75fba4..ccbbdff3 100644 --- a/src/LibHac/Fs/NcaUtils/NcaFsPatchInfo.cs +++ b/src/LibHac/FsSystem/NcaUtils/NcaFsPatchInfo.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { public struct NcaFsPatchInfo { diff --git a/src/LibHac/Fs/NcaUtils/NcaHeader.cs b/src/LibHac/FsSystem/NcaUtils/NcaHeader.cs similarity index 97% rename from src/LibHac/Fs/NcaUtils/NcaHeader.cs rename to src/LibHac/FsSystem/NcaUtils/NcaHeader.cs index 03a32ca3..7445129b 100644 --- a/src/LibHac/Fs/NcaUtils/NcaHeader.cs +++ b/src/LibHac/FsSystem/NcaUtils/NcaHeader.cs @@ -2,8 +2,9 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using LibHac.Fs; -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { public struct NcaHeader { @@ -17,7 +18,7 @@ namespace LibHac.Fs.NcaUtils public NcaHeader(IStorage headerStorage) { _header = new byte[HeaderSize]; - headerStorage.Read(_header.Span, 0); + headerStorage.Read(0, _header.Span); } public NcaHeader(Keyset keyset, IStorage headerStorage) @@ -44,9 +45,9 @@ namespace LibHac.Fs.NcaUtils set => Header.DistributionType = (byte)value; } - public ContentType ContentType + public NcaContentType ContentType { - get => (ContentType)Header.ContentType; + get => (NcaContentType)Header.ContentType; set => Header.ContentType = (byte)value; } @@ -188,7 +189,7 @@ namespace LibHac.Fs.NcaUtils public static byte[] DecryptHeader(Keyset keyset, IStorage storage) { var buf = new byte[HeaderSize]; - storage.Read(buf, 0); + storage.Read(0, buf); byte[] key1 = keyset.HeaderKey.AsSpan(0, 0x10).ToArray(); byte[] key2 = keyset.HeaderKey.AsSpan(0x10, 0x10).ToArray(); diff --git a/src/LibHac/Fs/NcaUtils/NcaKeyType.cs b/src/LibHac/FsSystem/NcaUtils/NcaKeyType.cs similarity index 76% rename from src/LibHac/Fs/NcaUtils/NcaKeyType.cs rename to src/LibHac/FsSystem/NcaUtils/NcaKeyType.cs index ac9d419e..589732e4 100644 --- a/src/LibHac/Fs/NcaUtils/NcaKeyType.cs +++ b/src/LibHac/FsSystem/NcaUtils/NcaKeyType.cs @@ -1,4 +1,4 @@ -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { internal enum NcaKeyType { diff --git a/src/LibHac/Fs/NcaUtils/NcaStructs.cs b/src/LibHac/FsSystem/NcaUtils/NcaStructs.cs similarity index 95% rename from src/LibHac/Fs/NcaUtils/NcaStructs.cs rename to src/LibHac/FsSystem/NcaUtils/NcaStructs.cs index cb583f34..fe4a2977 100644 --- a/src/LibHac/Fs/NcaUtils/NcaStructs.cs +++ b/src/LibHac/FsSystem/NcaUtils/NcaStructs.cs @@ -1,4 +1,4 @@ -namespace LibHac.Fs.NcaUtils +namespace LibHac.FsSystem.NcaUtils { public class TitleVersion { @@ -41,7 +41,7 @@ Logo }; - public enum ContentType + public enum NcaContentType { Program, Meta, diff --git a/src/LibHac/FsSystem/NullFile.cs b/src/LibHac/FsSystem/NullFile.cs new file mode 100644 index 00000000..4bfdbcf7 --- /dev/null +++ b/src/LibHac/FsSystem/NullFile.cs @@ -0,0 +1,53 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class NullFile : FileBase + { + private OpenMode Mode { get; } + + public NullFile() + { + Mode = OpenMode.ReadWrite; + } + + public NullFile(long length) : this() => Length = length; + + private long Length { get; } + + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) + { + bytesRead = 0; + + Result rc = ValidateReadParams(out long toRead, offset, destination.Length, Mode); + if (rc.IsFailure()) return rc; + + destination.Slice(0, (int)toRead).Clear(); + + bytesRead = toRead; + return Result.Success; + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) + { + return Result.Success; + } + + protected override Result FlushImpl() + { + return Result.Success; + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.UnsupportedOperation.Log(); + } + } +} diff --git a/src/LibHac/FsSystem/NullStorage.cs b/src/LibHac/FsSystem/NullStorage.cs new file mode 100644 index 00000000..81b4d23e --- /dev/null +++ b/src/LibHac/FsSystem/NullStorage.cs @@ -0,0 +1,44 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + /// + /// An that returns all zeros when read, and does nothing on write. + /// + public class NullStorage : StorageBase + { + private long Length { get; } + + public NullStorage() { } + public NullStorage(long length) => Length = length; + + + protected override Result ReadImpl(long offset, Span destination) + { + destination.Clear(); + return Result.Success; + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source) + { + return Result.Success; + } + + protected override Result FlushImpl() + { + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.NotImplemented.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + } +} diff --git a/src/LibHac/Fs/NxFileStream.cs b/src/LibHac/FsSystem/NxFileStream.cs similarity index 63% rename from src/LibHac/Fs/NxFileStream.cs rename to src/LibHac/FsSystem/NxFileStream.cs index 5720e81e..00d8593e 100644 --- a/src/LibHac/Fs/NxFileStream.cs +++ b/src/LibHac/FsSystem/NxFileStream.cs @@ -1,33 +1,38 @@ using System; using System.IO; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class NxFileStream : Stream { private IFile BaseFile { get; } private bool LeaveOpen { get; } + private OpenMode Mode { get; } private long _length; - public NxFileStream(IFile baseFile, bool leaveOpen) + public NxFileStream(IFile baseFile, bool leaveOpen) : this(baseFile, OpenMode.ReadWrite, leaveOpen) { } + + public NxFileStream(IFile baseFile, OpenMode mode, bool leaveOpen) { BaseFile = baseFile; + Mode = mode; LeaveOpen = leaveOpen; - _length = baseFile.GetSize(); + + baseFile.GetSize(out _length).ThrowIfFailure(); } public override int Read(byte[] buffer, int offset, int count) { - int toRead = (int)Math.Min(count, Length - Position); - BaseFile.Read(buffer.AsSpan(offset, toRead), Position); + BaseFile.Read(out long bytesRead, Position, buffer.AsSpan(offset, count)); - Position += toRead; - return toRead; + Position += bytesRead; + return (int)bytesRead; } public override void Write(byte[] buffer, int offset, int count) { - BaseFile.Write(buffer.AsSpan(offset, count), Position); + BaseFile.Write(Position, buffer.AsSpan(offset, count)); Position += count; } @@ -57,14 +62,14 @@ namespace LibHac.Fs public override void SetLength(long value) { - BaseFile.SetSize(value); + BaseFile.SetSize(value).ThrowIfFailure(); - _length = BaseFile.GetSize(); + BaseFile.GetSize(out _length).ThrowIfFailure(); } - public override bool CanRead => BaseFile.Mode.HasFlag(OpenMode.Read); + public override bool CanRead => Mode.HasFlag(OpenMode.Read); public override bool CanSeek => true; - public override bool CanWrite => BaseFile.Mode.HasFlag(OpenMode.Write); + public override bool CanWrite => Mode.HasFlag(OpenMode.Write); public override long Length => _length; public override long Position { get; set; } diff --git a/src/LibHac/FsSystem/PartitionDirectory.cs b/src/LibHac/FsSystem/PartitionDirectory.cs new file mode 100644 index 00000000..c66de8f2 --- /dev/null +++ b/src/LibHac/FsSystem/PartitionDirectory.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Text; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class PartitionDirectory : IDirectory + { + private PartitionFileSystem ParentFileSystem { get; } + private OpenDirectoryMode Mode { get; } + private int CurrentIndex { get; set; } + + public PartitionDirectory(PartitionFileSystem fs, string path, OpenDirectoryMode mode) + { + path = PathTools.Normalize(path); + + if (path != "/") throw new DirectoryNotFoundException(); + + ParentFileSystem = fs; + Mode = mode; + + CurrentIndex = 0; + } + + public Result Read(out long entriesRead, Span entryBuffer) + { + if (!Mode.HasFlag(OpenDirectoryMode.File)) + { + entriesRead = 0; + return Result.Success; + } + + int entriesRemaining = ParentFileSystem.Files.Length - CurrentIndex; + int toRead = Math.Min(entriesRemaining, entryBuffer.Length); + + for (int i = 0; i < toRead; i++) + { + PartitionFileEntry fileEntry = ParentFileSystem.Files[CurrentIndex]; + ref DirectoryEntry entry = ref entryBuffer[i]; + + Span nameUtf8 = Encoding.UTF8.GetBytes(fileEntry.Name); + + entry.Type = DirectoryEntryType.File; + entry.Size = fileEntry.Size; + + StringUtils.Copy(entry.Name, nameUtf8); + entry.Name[PathTools.MaxPathLength] = 0; + + CurrentIndex++; + } + + entriesRead = toRead; + return Result.Success; + } + + public Result GetEntryCount(out long entryCount) + { + int count = 0; + + if (Mode.HasFlag(OpenDirectoryMode.File)) + { + count += ParentFileSystem.Files.Length; + } + + entryCount = count; + return Result.Success; + } + } +} \ No newline at end of file diff --git a/src/LibHac/FsSystem/PartitionFile.cs b/src/LibHac/FsSystem/PartitionFile.cs new file mode 100644 index 00000000..7206fc9d --- /dev/null +++ b/src/LibHac/FsSystem/PartitionFile.cs @@ -0,0 +1,82 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class PartitionFile : FileBase + { + private IStorage BaseStorage { get; } + private long Offset { get; } + private long Size { get; } + private OpenMode Mode { get; } + + public PartitionFile(IStorage baseStorage, long offset, long size, OpenMode mode) + { + Mode = mode; + BaseStorage = baseStorage; + Offset = offset; + Size = size; + } + + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) + { + bytesRead = 0; + + Result rc = ValidateReadParams(out long toRead, offset, destination.Length, Mode); + if (rc.IsFailure()) return rc; + + long storageOffset = Offset + offset; + BaseStorage.Read(storageOffset, destination.Slice(0, (int)toRead)); + + bytesRead = toRead; + return Result.Success; + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) + { + Result rc = ValidateWriteParams(offset, source.Length, Mode, out bool isResizeNeeded); + if (rc.IsFailure()) return rc; + + if (isResizeNeeded) return ResultFs.UnsupportedOperationInPartitionFileSetSize.Log(); + + if (offset > Size) return ResultFs.ValueOutOfRange.Log(); + + rc = BaseStorage.Write(offset, source); + if (rc.IsFailure()) return rc; + + // N doesn't flush if the flag is set + if (options.HasFlag(WriteOption.Flush)) + { + return BaseStorage.Flush(); + } + + return Result.Success; + } + + protected override Result FlushImpl() + { + if (!Mode.HasFlag(OpenMode.Write)) + { + return BaseStorage.Flush(); + } + + return Result.Success; + } + + protected override Result GetSizeImpl(out long size) + { + size = Size; + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + if (!Mode.HasFlag(OpenMode.Write)) + { + return ResultFs.InvalidOpenModeForWrite.Log(); + } + + return ResultFs.UnsupportedOperationInPartitionFileSetSize.Log(); + } + } +} diff --git a/src/LibHac/Fs/PartitionFileSystem.cs b/src/LibHac/FsSystem/PartitionFileSystem.cs similarity index 65% rename from src/LibHac/Fs/PartitionFileSystem.cs rename to src/LibHac/FsSystem/PartitionFileSystem.cs index e8d75888..a3b8e8d1 100644 --- a/src/LibHac/Fs/PartitionFileSystem.cs +++ b/src/LibHac/FsSystem/PartitionFileSystem.cs @@ -3,10 +3,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { - public class PartitionFileSystem : IFileSystem + public class PartitionFileSystem : FileSystemBase { // todo Re-add way of checking a file hash public PartitionFileSystemHeader Header { get; } @@ -29,12 +30,13 @@ namespace LibHac.Fs BaseStorage = storage; } - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) { - return new PartitionDirectory(this, path, mode); + directory = new PartitionDirectory(this, path, mode); + return Result.Success; } - public IFile OpenFile(string path, OpenMode mode) + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) { path = PathTools.Normalize(path).TrimStart('/'); @@ -43,7 +45,8 @@ namespace LibHac.Fs ThrowHelper.ThrowResult(ResultFs.PathNotFound); } - return OpenFile(entry, mode); + file = OpenFile(entry, mode); + return Result.Success; } public IFile OpenFile(PartitionFileEntry entry, OpenMode mode) @@ -51,46 +54,39 @@ namespace LibHac.Fs return new PartitionFile(BaseStorage, HeaderSize + entry.Offset, entry.Size, mode); } - public DirectoryEntryType GetEntryType(string path) + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) { + entryType = DirectoryEntryType.NotFound; path = PathTools.Normalize(path); - if (path == "/") return DirectoryEntryType.Directory; + if (path == "/") + { + entryType = DirectoryEntryType.Directory; + return Result.Success; + } - if (FileDict.ContainsKey(path.TrimStart('/'))) return DirectoryEntryType.File; + if (FileDict.ContainsKey(path.TrimStart('/'))) + { + entryType = DirectoryEntryType.File; + return Result.Success; + } - return DirectoryEntryType.NotFound; + return ResultFs.PathNotFound.Log(); } - public void CreateDirectory(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyPartitionFileSystem); - public void CreateFile(string path, long size, CreateFileOptions options) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyPartitionFileSystem); - public void DeleteDirectory(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyPartitionFileSystem); - public void DeleteDirectoryRecursively(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyPartitionFileSystem); - public void CleanDirectoryRecursively(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyPartitionFileSystem); - public void DeleteFile(string path) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyPartitionFileSystem); - public void RenameDirectory(string srcPath, string dstPath) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyPartitionFileSystem); - public void RenameFile(string srcPath, string dstPath) => ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyPartitionFileSystem); + protected override Result CreateDirectoryImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log(); + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log(); + protected override Result DeleteDirectoryImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log(); + protected override Result DeleteDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log(); + protected override Result CleanDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log(); + protected override Result DeleteFileImpl(string path) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log(); + protected override Result RenameDirectoryImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log(); + protected override Result RenameFileImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperationModifyPartitionFileSystem.Log(); - public long GetFreeSpaceSize(string path) + protected override Result CommitImpl() { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; + return Result.Success; } - - public long GetTotalSpaceSize(string path) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; - } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; - } - - public void Commit() { } - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) => ThrowHelper.ThrowResult(ResultFs.NotImplemented); } public enum PartitionFileSystemType diff --git a/src/LibHac/Fs/PartitionFileSystemBuilder.cs b/src/LibHac/FsSystem/PartitionFileSystemBuilder.cs similarity index 87% rename from src/LibHac/Fs/PartitionFileSystemBuilder.cs rename to src/LibHac/FsSystem/PartitionFileSystemBuilder.cs index 62fdd00b..12cf4c82 100644 --- a/src/LibHac/Fs/PartitionFileSystemBuilder.cs +++ b/src/LibHac/FsSystem/PartitionFileSystemBuilder.cs @@ -4,8 +4,9 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class PartitionFileSystemBuilder { @@ -22,21 +23,23 @@ namespace LibHac.Fs /// public PartitionFileSystemBuilder(IFileSystem input) { - IDirectory rootDir = input.OpenDirectory("/", OpenDirectoryMode.Files); - - foreach (DirectoryEntry file in rootDir.Read().OrderBy(x => x.FullPath, StringComparer.Ordinal)) + foreach (DirectoryEntryEx entry in input.EnumerateEntries().OrderBy(x => x.FullPath, StringComparer.Ordinal)) { - AddFile(file.FullPath.TrimStart('/'), input.OpenFile(file.FullPath, OpenMode.Read)); + input.OpenFile(out IFile file, entry.FullPath, OpenMode.Read).ThrowIfFailure(); + + AddFile(entry.FullPath.TrimStart('/'), file); } } public void AddFile(string filename, IFile file) { + file.GetSize(out long fileSize).ThrowIfFailure(); + var entry = new Entry { Name = filename, File = file, - Length = file.GetSize(), + Length = fileSize, Offset = CurrentOffset, NameLength = Encoding.UTF8.GetByteCount(filename), HashOffset = 0, @@ -148,7 +151,12 @@ namespace LibHac.Fs if (entry.HashLength == 0) entry.HashLength = 0x200; var data = new byte[entry.HashLength]; - entry.File.Read(data, entry.HashOffset); + entry.File.Read(out long bytesRead, entry.HashOffset, data); + + if (bytesRead != entry.HashLength) + { + throw new ArgumentOutOfRangeException(); + } entry.Hash = sha.ComputeHash(data); } diff --git a/src/LibHac/Fs/PathParser.cs b/src/LibHac/FsSystem/PathParser.cs similarity index 99% rename from src/LibHac/Fs/PathParser.cs rename to src/LibHac/FsSystem/PathParser.cs index afe148cf..e6c3f51e 100644 --- a/src/LibHac/Fs/PathParser.cs +++ b/src/LibHac/FsSystem/PathParser.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; -namespace LibHac.Fs +namespace LibHac.FsSystem { /// /// Enumerates a file or directory path one segment at a time. diff --git a/src/LibHac/Fs/PathTools.cs b/src/LibHac/FsSystem/PathTools.cs similarity index 96% rename from src/LibHac/Fs/PathTools.cs rename to src/LibHac/FsSystem/PathTools.cs index 97dc2a76..742d0b51 100644 --- a/src/LibHac/Fs/PathTools.cs +++ b/src/LibHac/FsSystem/PathTools.cs @@ -1,12 +1,14 @@ using System; using System.Diagnostics; using System.Runtime.CompilerServices; +using LibHac.Common; +using LibHac.Fs; #if HAS_FILE_SYSTEM_NAME using System.IO.Enumeration; #endif -namespace LibHac.Fs +namespace LibHac.FsSystem { public static class PathTools { @@ -14,6 +16,9 @@ namespace LibHac.Fs public static readonly char MountSeparator = ':'; internal const int MountNameLength = 0xF; + // Todo: Remove + internal const int MaxPathLength = 0x300; + public static string Normalize(string inPath) { if (IsNormalized(inPath.AsSpan())) return inPath; @@ -47,6 +52,20 @@ namespace LibHac.Fs return normalized; } + public static Result Normalize(out U8Span normalizedPath, U8Span path) + { + if (path.Length == 0) + { + normalizedPath = path; + return Result.Success; + } + + // Todo: optimize + normalizedPath = new U8Span(Normalize(path.ToString())); + + return Result.Success; + } + // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. diff --git a/src/LibHac/FsSystem/ReadOnlyFile.cs b/src/LibHac/FsSystem/ReadOnlyFile.cs new file mode 100644 index 00000000..211f9d4d --- /dev/null +++ b/src/LibHac/FsSystem/ReadOnlyFile.cs @@ -0,0 +1,40 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class ReadOnlyFile : FileBase + { + private IFile BaseFile { get; } + + public ReadOnlyFile(IFile baseFile) + { + BaseFile = baseFile; + } + + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) + { + return BaseFile.Read(out bytesRead, offset, destination, options); + } + + protected override Result GetSizeImpl(out long size) + { + return BaseFile.GetSize(out size); + } + + protected override Result FlushImpl() + { + return Result.Success; + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) + { + return ResultFs.InvalidOpenModeForWrite.Log(); + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.InvalidOpenModeForWrite.Log(); + } + } +} diff --git a/src/LibHac/FsSystem/ReadOnlyFileSystem.cs b/src/LibHac/FsSystem/ReadOnlyFileSystem.cs new file mode 100644 index 00000000..a3c4860c --- /dev/null +++ b/src/LibHac/FsSystem/ReadOnlyFileSystem.cs @@ -0,0 +1,81 @@ +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class ReadOnlyFileSystem : FileSystemBase + { + private IFileSystem BaseFs { get; } + + public ReadOnlyFileSystem(IFileSystem baseFileSystem) + { + BaseFs = baseFileSystem; + } + + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) + { + return BaseFs.OpenDirectory(out directory, path, mode); + } + + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) + { + file = default; + + Result rc = BaseFs.OpenFile(out IFile baseFile, path, mode); + if (rc.IsFailure()) return rc; + + file = new ReadOnlyFile(baseFile); + return Result.Success; + } + + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) + { + return BaseFs.GetEntryType(out entryType, path); + } + + protected override Result GetFreeSpaceSizeImpl(out long freeSpace, string path) + { + freeSpace = 0; + return Result.Success; + + // FS does: + // return ResultFs.UnsupportedOperationReadOnlyFileSystemGetSpace.Log(); + } + + protected override Result GetTotalSpaceSizeImpl(out long totalSpace, string path) + { + return BaseFs.GetTotalSpaceSize(out totalSpace, path); + + // FS does: + // return ResultFs.UnsupportedOperationReadOnlyFileSystemGetSpace.Log(); + } + + protected override Result GetFileTimeStampRawImpl(out FileTimeStampRaw timeStamp, string path) + { + return BaseFs.GetFileTimeStampRaw(out timeStamp, path); + + // FS does: + // return ResultFs.NotImplemented.Log(); + } + + protected override Result CommitImpl() + { + return Result.Success; + } + + protected override Result CreateDirectoryImpl(string path) => ResultFs.UnsupportedOperationModifyReadOnlyFileSystem.Log(); + + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) => ResultFs.UnsupportedOperationModifyReadOnlyFileSystem.Log(); + + protected override Result DeleteDirectoryImpl(string path) => ResultFs.UnsupportedOperationModifyReadOnlyFileSystem.Log(); + + protected override Result DeleteDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperationModifyReadOnlyFileSystem.Log(); + + protected override Result CleanDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperationModifyReadOnlyFileSystem.Log(); + + protected override Result DeleteFileImpl(string path) => ResultFs.UnsupportedOperationModifyReadOnlyFileSystem.Log(); + + protected override Result RenameDirectoryImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperationModifyReadOnlyFileSystem.Log(); + + protected override Result RenameFileImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperationModifyReadOnlyFileSystem.Log(); + } +} diff --git a/src/LibHac/Fs/RomFs/HierarchicalRomFileTable.cs b/src/LibHac/FsSystem/RomFs/HierarchicalRomFileTable.cs similarity index 99% rename from src/LibHac/Fs/RomFs/HierarchicalRomFileTable.cs rename to src/LibHac/FsSystem/RomFs/HierarchicalRomFileTable.cs index b73b2efd..8d455b2a 100644 --- a/src/LibHac/Fs/RomFs/HierarchicalRomFileTable.cs +++ b/src/LibHac/FsSystem/RomFs/HierarchicalRomFileTable.cs @@ -1,7 +1,8 @@ using System; using System.Runtime.InteropServices; +using LibHac.Fs; -namespace LibHac.Fs.RomFs +namespace LibHac.FsSystem.RomFs { /// /// Represents the file table used by the RomFS format. diff --git a/src/LibHac/Fs/RomFs/RomFsBuilder.cs b/src/LibHac/FsSystem/RomFs/RomFsBuilder.cs similarity index 86% rename from src/LibHac/Fs/RomFs/RomFsBuilder.cs rename to src/LibHac/FsSystem/RomFs/RomFsBuilder.cs index 0d745d31..5f95e172 100644 --- a/src/LibHac/Fs/RomFs/RomFsBuilder.cs +++ b/src/LibHac/FsSystem/RomFs/RomFsBuilder.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using LibHac.Fs; -namespace LibHac.Fs.RomFs +namespace LibHac.FsSystem.RomFs { /// /// Builds a RomFS from a collection of files. @@ -32,10 +33,12 @@ namespace LibHac.Fs.RomFs /// public RomFsBuilder(IFileSystem input) { - foreach (DirectoryEntry file in input.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File) + foreach (DirectoryEntryEx entry in input.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File) .OrderBy(x => x.FullPath, StringComparer.Ordinal)) { - AddFile(file.FullPath, input.OpenFile(file.FullPath, OpenMode.Read)); + input.OpenFile(out IFile file, entry.FullPath, OpenMode.Read).ThrowIfFailure(); + + AddFile(entry.FullPath, file); } } @@ -47,7 +50,7 @@ namespace LibHac.Fs.RomFs public void AddFile(string path, IFile file) { var fileInfo = new RomFileInfo(); - long fileSize = file.GetSize(); + file.GetSize(out long fileSize).ThrowIfFailure(); fileInfo.Offset = CurrentOffset; fileInfo.Length = fileSize; @@ -81,7 +84,11 @@ namespace LibHac.Fs.RomFs sources.Add(new MemoryStorage(header)); sources.AddRange(Sources); - long fileLength = sources.Sum(x => x.GetSize()); + long fileLength = sources.Sum(x => + { + x.GetSize(out long fileSize).ThrowIfFailure(); + return fileSize; + }); headerWriter.Write((long)HeaderSize); diff --git a/src/LibHac/Fs/RomFs/RomFsDictionary.cs b/src/LibHac/FsSystem/RomFs/RomFsDictionary.cs similarity index 99% rename from src/LibHac/Fs/RomFs/RomFsDictionary.cs rename to src/LibHac/FsSystem/RomFs/RomFsDictionary.cs index b1a5142f..9989f8a7 100644 --- a/src/LibHac/Fs/RomFs/RomFsDictionary.cs +++ b/src/LibHac/FsSystem/RomFs/RomFsDictionary.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using LibHac.Fs; -namespace LibHac.Fs.RomFs +namespace LibHac.FsSystem.RomFs { // todo: Change constraint to "unmanaged" after updating to // a newer SDK https://github.com/dotnet/csharplang/issues/1937 diff --git a/src/LibHac/FsSystem/RomFs/RomFsDirectory.cs b/src/LibHac/FsSystem/RomFs/RomFsDirectory.cs new file mode 100644 index 00000000..12c19acb --- /dev/null +++ b/src/LibHac/FsSystem/RomFs/RomFsDirectory.cs @@ -0,0 +1,88 @@ +using System; +using System.Text; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem.RomFs +{ + public class RomFsDirectory : IDirectory + { + private RomFsFileSystem ParentFileSystem { get; } + + private OpenDirectoryMode Mode { get; } + + private FindPosition InitialPosition { get; } + private FindPosition _currentPosition; + + public RomFsDirectory(RomFsFileSystem fs, FindPosition position, OpenDirectoryMode mode) + { + ParentFileSystem = fs; + InitialPosition = position; + _currentPosition = position; + Mode = mode; + } + + public Result Read(out long entriesRead, Span entryBuffer) + { + return ReadImpl(out entriesRead, ref _currentPosition, entryBuffer); + } + + public Result GetEntryCount(out long entryCount) + { + FindPosition position = InitialPosition; + + return ReadImpl(out entryCount, ref position, Span.Empty); + } + + private Result ReadImpl(out long entriesRead, ref FindPosition position, Span entryBuffer) + { + HierarchicalRomFileTable tab = ParentFileSystem.FileTable; + + int i = 0; + + if (Mode.HasFlag(OpenDirectoryMode.Directory)) + { + while ((entryBuffer.IsEmpty || i < entryBuffer.Length) && tab.FindNextDirectory(ref position, out string name)) + { + if (!entryBuffer.IsEmpty) + { + ref DirectoryEntry entry = ref entryBuffer[i]; + Span nameUtf8 = Encoding.UTF8.GetBytes(name); + + StringUtils.Copy(entry.Name, nameUtf8); + entry.Name[PathTools.MaxPathLength] = 0; + + entry.Type = DirectoryEntryType.Directory; + entry.Size = 0; + } + + i++; + } + } + + if (Mode.HasFlag(OpenDirectoryMode.File)) + { + while ((entryBuffer.IsEmpty || i < entryBuffer.Length) && tab.FindNextFile(ref position, out RomFileInfo info, out string name)) + { + if (!entryBuffer.IsEmpty) + { + ref DirectoryEntry entry = ref entryBuffer[i]; + Span nameUtf8 = Encoding.UTF8.GetBytes(name); + + StringUtils.Copy(entry.Name, nameUtf8); + entry.Name[PathTools.MaxPathLength] = 0; + + entry.Type = DirectoryEntryType.File; + entry.Size = info.Length; + } + + i++; + } + } + + entriesRead = i; + + return Result.Success; + } + } +} diff --git a/src/LibHac/Fs/RomFs/RomFsEntries.cs b/src/LibHac/FsSystem/RomFs/RomFsEntries.cs similarity index 97% rename from src/LibHac/Fs/RomFs/RomFsEntries.cs rename to src/LibHac/FsSystem/RomFs/RomFsEntries.cs index 5c12b8cd..8e6a4e7e 100644 --- a/src/LibHac/Fs/RomFs/RomFsEntries.cs +++ b/src/LibHac/FsSystem/RomFs/RomFsEntries.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace LibHac.Fs.RomFs +namespace LibHac.FsSystem.RomFs { internal ref struct RomEntryKey { diff --git a/src/LibHac/FsSystem/RomFs/RomFsFile.cs b/src/LibHac/FsSystem/RomFs/RomFsFile.cs new file mode 100644 index 00000000..11d05b38 --- /dev/null +++ b/src/LibHac/FsSystem/RomFs/RomFsFile.cs @@ -0,0 +1,57 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem.RomFs +{ + public class RomFsFile : FileBase + { + private IStorage BaseStorage { get; } + private long Offset { get; } + private long Size { get; } + + public RomFsFile(IStorage baseStorage, long offset, long size) + { + BaseStorage = baseStorage; + Offset = offset; + Size = size; + } + + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) + { + bytesRead = default; + + Result rc = ValidateReadParams(out long toRead, offset, destination.Length, OpenMode.Read); + if (rc.IsFailure()) return rc; + + long storageOffset = Offset + offset; + + rc = BaseStorage.Read(storageOffset, destination.Slice(0, (int)toRead)); + if (rc.IsFailure()) return rc; + + bytesRead = toRead; + + return Result.Success; + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) + { + return ResultFs.UnsupportedOperationModifyRomFsFile.Log(); + } + + protected override Result FlushImpl() + { + return Result.Success; + } + + protected override Result GetSizeImpl(out long size) + { + size = Size; + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + return ResultFs.UnsupportedOperationModifyRomFsFile.Log(); + } + } +} diff --git a/src/LibHac/Fs/RomFs/RomFsFileSystem.cs b/src/LibHac/FsSystem/RomFs/RomFsFileSystem.cs similarity index 51% rename from src/LibHac/Fs/RomFs/RomFsFileSystem.cs rename to src/LibHac/FsSystem/RomFs/RomFsFileSystem.cs index ee944186..56dafb95 100644 --- a/src/LibHac/Fs/RomFs/RomFsFileSystem.cs +++ b/src/LibHac/FsSystem/RomFs/RomFsFileSystem.cs @@ -1,8 +1,8 @@ -using System; +using LibHac.Fs; -namespace LibHac.Fs.RomFs +namespace LibHac.FsSystem.RomFs { - public class RomFsFileSystem : IFileSystem + public class RomFsFileSystem : FileSystemBase { public RomfsHeader Header { get; } @@ -22,52 +22,63 @@ namespace LibHac.Fs.RomFs FileTable = new HierarchicalRomFileTable(dirHashTable, dirEntryTable, fileHashTable, fileEntryTable); } - public DirectoryEntryType GetEntryType(string path) + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) { + entryType = DirectoryEntryType.NotFound; path = PathTools.Normalize(path); if (FileTable.TryOpenFile(path, out RomFileInfo _)) { - return DirectoryEntryType.File; + entryType = DirectoryEntryType.File; + return Result.Success; } if (FileTable.TryOpenDirectory(path, out FindPosition _)) { - return DirectoryEntryType.Directory; + entryType = DirectoryEntryType.Directory; + return Result.Success; } - return DirectoryEntryType.NotFound; + return ResultFs.PathNotFound.Log(); } - public void Commit() { } - - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) + protected override Result CommitImpl() { + return Result.Success; + } + + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) + { + directory = default; path = PathTools.Normalize(path); if (!FileTable.TryOpenDirectory(path, out FindPosition position)) { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); + return ResultFs.PathNotFound.Log(); } - return new RomFsDirectory(this, path, position, mode); + directory = new RomFsDirectory(this, position, mode); + return Result.Success; } - public IFile OpenFile(string path, OpenMode mode) + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) { + file = default; path = PathTools.Normalize(path); if (!FileTable.TryOpenFile(path, out RomFileInfo info)) { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); + return ResultFs.PathNotFound.Log(); } if (mode != OpenMode.Read) { - ThrowHelper.ThrowResult(ResultFs.InvalidArgument, "RomFs files must be opened read-only."); + // RomFs files must be opened read-only. + return ResultFs.InvalidArgument.Log(); } - return new RomFsFile(BaseStorage, Header.DataOffset + info.Offset, info.Length); + file = new RomFsFile(BaseStorage, Header.DataOffset + info.Offset, info.Length); + return Result.Success; } public IStorage GetBaseStorage() @@ -75,50 +86,26 @@ namespace LibHac.Fs.RomFs return BaseStorage; } - public void CreateDirectory(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFileSystem); + protected override Result CreateDirectoryImpl(string path) => ResultFs.UnsupportedOperationModifyRomFsFileSystem.Log(); + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) => ResultFs.UnsupportedOperationModifyRomFsFileSystem.Log(); + protected override Result DeleteDirectoryImpl(string path) => ResultFs.UnsupportedOperationModifyRomFsFileSystem.Log(); + protected override Result DeleteDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperationModifyRomFsFileSystem.Log(); + protected override Result CleanDirectoryRecursivelyImpl(string path) => ResultFs.UnsupportedOperationModifyRomFsFileSystem.Log(); + protected override Result DeleteFileImpl(string path) => ResultFs.UnsupportedOperationModifyRomFsFileSystem.Log(); + protected override Result RenameDirectoryImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperationModifyRomFsFileSystem.Log(); + protected override Result RenameFileImpl(string oldPath, string newPath) => ResultFs.UnsupportedOperationModifyRomFsFileSystem.Log(); - public void CreateFile(string path, long size, CreateFileOptions options) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFileSystem); - - public void DeleteDirectory(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFileSystem); - - public void DeleteDirectoryRecursively(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFileSystem); - - public void CleanDirectoryRecursively(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFileSystem); - - public void DeleteFile(string path) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFileSystem); - - public void RenameDirectory(string srcPath, string dstPath) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFileSystem); - - public void RenameFile(string srcPath, string dstPath) => - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyRomFsFileSystem); - - public long GetFreeSpaceSize(string path) + protected override Result GetFreeSpaceSizeImpl(out long freeSpace, string path) { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationRomFsFileSystemGetSpace); - return default; + freeSpace = default; + return ResultFs.UnsupportedOperationRomFsFileSystemGetSpace.Log(); } - public long GetTotalSpaceSize(string path) + protected override Result GetTotalSpaceSizeImpl(out long totalSpace, string path) { - ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationRomFsFileSystemGetSpace); - return default; + totalSpace = default; + return ResultFs.UnsupportedOperationRomFsFileSystemGetSpace.Log(); } - - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) => - ThrowHelper.ThrowResult(ResultFs.NotImplemented); } public class RomfsHeader diff --git a/src/LibHac/Fs/Save/AllocationTable.cs b/src/LibHac/FsSystem/Save/AllocationTable.cs similarity index 98% rename from src/LibHac/Fs/Save/AllocationTable.cs rename to src/LibHac/FsSystem/Save/AllocationTable.cs index ad85e744..57c221b3 100644 --- a/src/LibHac/Fs/Save/AllocationTable.cs +++ b/src/LibHac/FsSystem/Save/AllocationTable.cs @@ -2,8 +2,9 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class AllocationTable { @@ -356,7 +357,7 @@ namespace LibHac.Fs.Save Span buffer = MemoryMarshal.Cast(entries.Slice(0, entriesToRead)); - BaseStorage.Read(buffer, offset); + BaseStorage.Read(offset, buffer).ThrowIfFailure(); } private AllocationTableEntry ReadEntry(int entryIndex) @@ -364,7 +365,7 @@ namespace LibHac.Fs.Save Span bytes = stackalloc byte[EntrySize]; int offset = entryIndex * EntrySize; - BaseStorage.Read(bytes, offset); + BaseStorage.Read(offset, bytes).ThrowIfFailure(); return GetEntryFromBytes(bytes); } @@ -377,7 +378,7 @@ namespace LibHac.Fs.Save ref AllocationTableEntry newEntry = ref GetEntryFromBytes(bytes); newEntry = entry; - BaseStorage.Write(bytes, offset); + BaseStorage.Write(offset, bytes).ThrowIfFailure(); } // ReSharper disable once UnusedMember.Local diff --git a/src/LibHac/Fs/Save/AllocationTableIterator.cs b/src/LibHac/FsSystem/Save/AllocationTableIterator.cs similarity index 98% rename from src/LibHac/Fs/Save/AllocationTableIterator.cs rename to src/LibHac/FsSystem/Save/AllocationTableIterator.cs index e97b79e9..886eb5be 100644 --- a/src/LibHac/Fs/Save/AllocationTableIterator.cs +++ b/src/LibHac/FsSystem/Save/AllocationTableIterator.cs @@ -1,6 +1,6 @@ using System; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class AllocationTableIterator { diff --git a/src/LibHac/Fs/Save/AllocationTableStorage.cs b/src/LibHac/FsSystem/Save/AllocationTableStorage.cs similarity index 76% rename from src/LibHac/Fs/Save/AllocationTableStorage.cs rename to src/LibHac/FsSystem/Save/AllocationTableStorage.cs index d210bd10..f68dfe16 100644 --- a/src/LibHac/Fs/Save/AllocationTableStorage.cs +++ b/src/LibHac/FsSystem/Save/AllocationTableStorage.cs @@ -1,7 +1,8 @@ using System; using System.IO; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class AllocationTableStorage : StorageBase { @@ -22,7 +23,7 @@ namespace LibHac.Fs.Save _length = initialBlock == -1 ? 0 : table.GetListLength(initialBlock) * blockSize; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { var iterator = new AllocationTableIterator(Fat, InitialBlock); @@ -41,15 +42,18 @@ namespace LibHac.Fs.Save int remainingInSegment = iterator.CurrentSegmentSize * BlockSize - segmentPos; int bytesToRead = Math.Min(remaining, remainingInSegment); - BaseStorage.Read(destination.Slice(outPos, bytesToRead), physicalOffset); + Result rc = BaseStorage.Read(physicalOffset, destination.Slice(outPos, bytesToRead)); + if (rc.IsFailure()) return rc; outPos += bytesToRead; inPos += bytesToRead; remaining -= bytesToRead; } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { var iterator = new AllocationTableIterator(Fat, InitialBlock); @@ -68,27 +72,34 @@ namespace LibHac.Fs.Save int remainingInSegment = iterator.CurrentSegmentSize * BlockSize - segmentPos; int bytesToWrite = Math.Min(remaining, remainingInSegment); - BaseStorage.Write(source.Slice(outPos, bytesToWrite), physicalOffset); + Result rc = BaseStorage.Write(physicalOffset, source.Slice(outPos, bytesToWrite)); + if (rc.IsFailure()) return rc; outPos += bytesToWrite; inPos += bytesToWrite; remaining -= bytesToWrite; } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { - BaseStorage.Flush(); + return BaseStorage.Flush(); } - public override long GetSize() => _length; + protected override Result GetSizeImpl(out long size) + { + size = _length; + return Result.Success; + } - public override void SetSize(long size) + protected override Result SetSizeImpl(long size) { int oldBlockCount = (int)Util.DivideByRoundUp(_length, BlockSize); int newBlockCount = (int)Util.DivideByRoundUp(size, BlockSize); - if (oldBlockCount == newBlockCount) return; + if (oldBlockCount == newBlockCount) return Result.Success; if (oldBlockCount == 0) { @@ -97,7 +108,7 @@ namespace LibHac.Fs.Save _length = newBlockCount * BlockSize; - return; + return Result.Success; } if (newBlockCount == 0) @@ -107,7 +118,7 @@ namespace LibHac.Fs.Save InitialBlock = int.MinValue; _length = 0; - return; + return Result.Success; } if (newBlockCount > oldBlockCount) @@ -124,6 +135,8 @@ namespace LibHac.Fs.Save } _length = newBlockCount * BlockSize; + + return Result.Success; } } } diff --git a/src/LibHac/Fs/Save/DuplexBitmap.cs b/src/LibHac/FsSystem/Save/DuplexBitmap.cs similarity index 95% rename from src/LibHac/Fs/Save/DuplexBitmap.cs rename to src/LibHac/FsSystem/Save/DuplexBitmap.cs index 649ef785..993eb7d5 100644 --- a/src/LibHac/Fs/Save/DuplexBitmap.cs +++ b/src/LibHac/FsSystem/Save/DuplexBitmap.cs @@ -1,8 +1,9 @@ using System; using System.Collections; using System.IO; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class DuplexBitmap { diff --git a/src/LibHac/Fs/Save/DuplexStorage.cs b/src/LibHac/FsSystem/Save/DuplexStorage.cs similarity index 54% rename from src/LibHac/Fs/Save/DuplexStorage.cs rename to src/LibHac/FsSystem/Save/DuplexStorage.cs index 890a519c..e36fa7cb 100644 --- a/src/LibHac/Fs/Save/DuplexStorage.cs +++ b/src/LibHac/FsSystem/Save/DuplexStorage.cs @@ -1,6 +1,7 @@ using System; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class DuplexStorage : StorageBase { @@ -10,7 +11,7 @@ namespace LibHac.Fs.Save private IStorage DataB { get; } private DuplexBitmap Bitmap { get; } - private long _length; + private long Length { get; } public DuplexStorage(IStorage dataA, IStorage dataB, IStorage bitmap, int blockSize) { @@ -19,16 +20,22 @@ namespace LibHac.Fs.Save BitmapStorage = bitmap; BlockSize = blockSize; - Bitmap = new DuplexBitmap(BitmapStorage, (int)(bitmap.GetSize() * 8)); - _length = DataA.GetSize(); + bitmap.GetSize(out long bitmapSize).ThrowIfFailure(); + + Bitmap = new DuplexBitmap(BitmapStorage, (int)(bitmapSize * 8)); + DataA.GetSize(out long dataSize).ThrowIfFailure(); + Length = dataSize; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { long inPos = offset; int outPos = 0; int remaining = destination.Length; + if (!IsRangeValid(offset, destination.Length, Length)) + return ResultFs.ValueOutOfRange.Log(); + while (remaining > 0) { int blockNum = (int)(inPos / BlockSize); @@ -38,20 +45,26 @@ namespace LibHac.Fs.Save IStorage data = Bitmap.Bitmap[blockNum] ? DataB : DataA; - data.Read(destination.Slice(outPos, bytesToRead), inPos); + Result rc = data.Read(inPos, destination.Slice(outPos, bytesToRead)); + if (rc.IsFailure()) return rc; outPos += bytesToRead; inPos += bytesToRead; remaining -= bytesToRead; } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { long inPos = offset; int outPos = 0; int remaining = source.Length; + if (!IsRangeValid(offset, source.Length, Length)) + return ResultFs.ValueOutOfRange.Log(); + while (remaining > 0) { int blockNum = (int)(inPos / BlockSize); @@ -61,26 +74,47 @@ namespace LibHac.Fs.Save IStorage data = Bitmap.Bitmap[blockNum] ? DataB : DataA; - data.Write(source.Slice(outPos, bytesToWrite), inPos); + Result rc = data.Write(inPos, source.Slice(outPos, bytesToWrite)); + if (rc.IsFailure()) return rc; outPos += bytesToWrite; inPos += bytesToWrite; remaining -= bytesToWrite; } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { - BitmapStorage?.Flush(); - DataA?.Flush(); - DataB?.Flush(); + Result rc = BitmapStorage.Flush(); + if (rc.IsFailure()) return rc; + + rc = DataA.Flush(); + if (rc.IsFailure()) return rc; + + rc = DataB.Flush(); + if (rc.IsFailure()) return rc; + + return Result.Success; } - public override long GetSize() => _length; + protected override Result SetSizeImpl(long size) + { + return ResultFs.NotImplemented.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } public void FsTrim() { - int blockCount = (int)(DataA.GetSize() / BlockSize); + DataA.GetSize(out long dataSize).ThrowIfFailure(); + + int blockCount = (int)(dataSize / BlockSize); for (int i = 0; i < blockCount; i++) { diff --git a/src/LibHac/Fs/Save/Header.cs b/src/LibHac/FsSystem/Save/Header.cs similarity index 97% rename from src/LibHac/Fs/Save/Header.cs rename to src/LibHac/FsSystem/Save/Header.cs index d6d06a67..39fb32cb 100644 --- a/src/LibHac/Fs/Save/Header.cs +++ b/src/LibHac/FsSystem/Save/Header.cs @@ -1,7 +1,8 @@ using System; using System.IO; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class Header { @@ -280,13 +281,14 @@ namespace LibHac.Fs.Save } } - public enum SaveDataType + public enum SaveDataType : byte { - SystemSaveData, - SaveData, - BcatDeliveryCacheStorage, - DeviceSaveData, - TemporaryStorage, - CacheStorage + SystemSaveData = 0, + SaveData = 1, + BcatDeliveryCacheStorage = 2, + DeviceSaveData = 3, + TemporaryStorage = 4, + CacheStorage = 5, + BcatSystemStorage = 6 } } diff --git a/src/LibHac/Fs/Save/HierarchicalDuplexStorage.cs b/src/LibHac/FsSystem/Save/HierarchicalDuplexStorage.cs similarity index 61% rename from src/LibHac/Fs/Save/HierarchicalDuplexStorage.cs rename to src/LibHac/FsSystem/Save/HierarchicalDuplexStorage.cs index df24abd4..132b3ad5 100644 --- a/src/LibHac/Fs/Save/HierarchicalDuplexStorage.cs +++ b/src/LibHac/FsSystem/Save/HierarchicalDuplexStorage.cs @@ -1,12 +1,13 @@ using System; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class HierarchicalDuplexStorage : StorageBase { private DuplexStorage[] Layers { get; } private DuplexStorage DataLayer { get; } - private long _length; + private long Length { get; } public HierarchicalDuplexStorage(DuplexFsLayerInfo[] layers, bool masterBit) { @@ -29,25 +30,35 @@ namespace LibHac.Fs.Save } DataLayer = Layers[Layers.Length - 1]; - _length = DataLayer.GetSize(); + DataLayer.GetSize(out long dataSize).ThrowIfFailure(); + Length = dataSize; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { - DataLayer.Read(destination, offset); + return DataLayer.Read(offset, destination); } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { - DataLayer.Write(source, offset); + return DataLayer.Write(offset, source); } - public override void Flush() + protected override Result FlushImpl() { - DataLayer.Flush(); + return DataLayer.Flush(); } - public override long GetSize() => _length; + protected override Result SetSizeImpl(long size) + { + return ResultFs.NotImplemented.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } public void FsTrim() { diff --git a/src/LibHac/Fs/Save/HierarchicalSaveFileTable.cs b/src/LibHac/FsSystem/Save/HierarchicalSaveFileTable.cs similarity index 97% rename from src/LibHac/Fs/Save/HierarchicalSaveFileTable.cs rename to src/LibHac/FsSystem/Save/HierarchicalSaveFileTable.cs index 07b8ac5e..236af46b 100644 --- a/src/LibHac/Fs/Save/HierarchicalSaveFileTable.cs +++ b/src/LibHac/FsSystem/Save/HierarchicalSaveFileTable.cs @@ -1,8 +1,9 @@ using System; using System.IO; using System.Runtime.InteropServices; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class HierarchicalSaveFileTable { @@ -338,11 +339,11 @@ namespace LibHac.Fs.Save FileTable.ChangeKey(ref oldKey, ref newKey); } - public void RenameDirectory(string srcPath, string dstPath) + public Result RenameDirectory(string srcPath, string dstPath) { if (srcPath == dstPath || TryOpenFile(dstPath, out _) || TryOpenDirectory(dstPath, out _)) { - throw new IOException(Messages.DestPathAlreadyExists); + return ResultFs.PathAlreadyExists.Log(); } ReadOnlySpan oldPathBytes = Util.GetUtf8Bytes(srcPath); @@ -350,19 +351,19 @@ namespace LibHac.Fs.Save if (!FindPathRecursive(oldPathBytes, out SaveEntryKey oldKey)) { - throw new DirectoryNotFoundException(); + return ResultFs.PathNotFound.Log(); } int dirIndex = DirectoryTable.GetIndexFromKey(ref oldKey).Index; if (!FindPathRecursive(newPathBytes, out SaveEntryKey newKey)) { - throw new IOException(Messages.PartialPathNotFound); + return ResultFs.PathNotFound.Log(); } if (PathTools.IsSubPath(oldPathBytes, newPathBytes)) { - ThrowHelper.ThrowResult(ResultFs.DestinationIsSubPathOfSource); + return ResultFs.DestinationIsSubPathOfSource.Log(); } if (oldKey.Parent != newKey.Parent) @@ -372,6 +373,8 @@ namespace LibHac.Fs.Save } DirectoryTable.ChangeKey(ref oldKey, ref newKey); + + return Result.Success; } public bool TryOpenDirectory(string path, out SaveFindPosition position) diff --git a/src/LibHac/FsSystem/Save/ISaveDataExtraDataAccessor.cs b/src/LibHac/FsSystem/Save/ISaveDataExtraDataAccessor.cs new file mode 100644 index 00000000..707e172a --- /dev/null +++ b/src/LibHac/FsSystem/Save/ISaveDataExtraDataAccessor.cs @@ -0,0 +1,9 @@ +namespace LibHac.FsSystem.Save +{ + public interface ISaveDataExtraDataAccessor + { + Result Write(ExtraData data); + Result Commit(); + Result Read(out ExtraData data); + } +} diff --git a/src/LibHac/Fs/Save/JournalMap.cs b/src/LibHac/FsSystem/Save/JournalMap.cs similarity index 98% rename from src/LibHac/Fs/Save/JournalMap.cs rename to src/LibHac/FsSystem/Save/JournalMap.cs index d49af98c..dd13c552 100644 --- a/src/LibHac/Fs/Save/JournalMap.cs +++ b/src/LibHac/FsSystem/Save/JournalMap.cs @@ -1,6 +1,7 @@ using System.IO; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class JournalMap { diff --git a/src/LibHac/Fs/Save/JournalStorage.cs b/src/LibHac/FsSystem/Save/JournalStorage.cs similarity index 67% rename from src/LibHac/Fs/Save/JournalStorage.cs rename to src/LibHac/FsSystem/Save/JournalStorage.cs index 91e4f809..1ac69416 100644 --- a/src/LibHac/Fs/Save/JournalStorage.cs +++ b/src/LibHac/FsSystem/Save/JournalStorage.cs @@ -1,8 +1,9 @@ using System; using System.Collections; using System.IO; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class JournalStorage : StorageBase { @@ -14,7 +15,8 @@ namespace LibHac.Fs.Save public int BlockSize { get; } - private long _length; + private long Length { get; } + private bool LeaveOpen { get; } public JournalStorage(IStorage baseStorage, IStorage header, JournalMapParams mapInfo, bool leaveOpen) { @@ -26,17 +28,20 @@ namespace LibHac.Fs.Save Map = new JournalMap(mapHeader, mapInfo); BlockSize = (int)Header.BlockSize; - _length = Header.TotalSize - Header.JournalSize; + Length = Header.TotalSize - Header.JournalSize; - if (!leaveOpen) ToDispose.Add(baseStorage); + LeaveOpen = leaveOpen; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { long inPos = offset; int outPos = 0; int remaining = destination.Length; + if (!IsRangeValid(offset, destination.Length, Length)) + return ResultFs.ValueOutOfRange.Log(); + while (remaining > 0) { int blockNum = (int)(inPos / BlockSize); @@ -46,20 +51,26 @@ namespace LibHac.Fs.Save int bytesToRead = Math.Min(remaining, BlockSize - blockPos); - BaseStorage.Read(destination.Slice(outPos, bytesToRead), physicalOffset); + Result rc = BaseStorage.Read(physicalOffset, destination.Slice(outPos, bytesToRead)); + if (rc.IsFailure()) return rc; outPos += bytesToRead; inPos += bytesToRead; remaining -= bytesToRead; } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { long inPos = offset; int outPos = 0; int remaining = source.Length; + if (!IsRangeValid(offset, source.Length, Length)) + return ResultFs.ValueOutOfRange.Log(); + while (remaining > 0) { int blockNum = (int)(inPos / BlockSize); @@ -69,20 +80,43 @@ namespace LibHac.Fs.Save int bytesToWrite = Math.Min(remaining, BlockSize - blockPos); - BaseStorage.Write(source.Slice(outPos, bytesToWrite), physicalOffset); + Result rc = BaseStorage.Write(physicalOffset, source.Slice(outPos, bytesToWrite)); + if (rc.IsFailure()) return rc; outPos += bytesToWrite; inPos += bytesToWrite; remaining -= bytesToWrite; } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { - BaseStorage.Flush(); + return BaseStorage.Flush(); } - public override long GetSize() => _length; + protected override Result SetSizeImpl(long size) + { + return ResultFs.NotImplemented.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!LeaveOpen) + { + BaseStorage?.Dispose(); + } + } + } public IStorage GetBaseStorage() => BaseStorage.AsReadOnly(); public IStorage GetHeaderStorage() => HeaderStorage.AsReadOnly(); diff --git a/src/LibHac/Fs/Save/RemapStorage.cs b/src/LibHac/FsSystem/Save/RemapStorage.cs similarity index 83% rename from src/LibHac/Fs/Save/RemapStorage.cs rename to src/LibHac/FsSystem/Save/RemapStorage.cs index d1de2442..32432176 100644 --- a/src/LibHac/Fs/Save/RemapStorage.cs +++ b/src/LibHac/FsSystem/Save/RemapStorage.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public class RemapStorage : StorageBase { @@ -12,6 +13,7 @@ namespace LibHac.Fs.Save private IStorage BaseStorage { get; } private IStorage HeaderStorage { get; } private IStorage MapEntryStorage { get; } + private bool LeaveOpen { get; } private RemapHeader Header { get; } public MapEntry[] MapEntries { get; set; } @@ -41,14 +43,14 @@ namespace LibHac.Fs.Save MapEntries[i] = new MapEntry(reader); } - if (!leaveOpen) ToDispose.Add(BaseStorage); + LeaveOpen = leaveOpen; Segments = InitSegments(Header, MapEntries); } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { - if (destination.Length == 0) return; + if (destination.Length == 0) return Result.Success; MapEntry entry = GetMapEntry(offset); @@ -61,7 +63,7 @@ namespace LibHac.Fs.Save long entryPos = inPos - entry.VirtualOffset; int bytesToRead = (int)Math.Min(entry.VirtualOffsetEnd - inPos, remaining); - BaseStorage.Read(destination.Slice(outPos, bytesToRead), entry.PhysicalOffset + entryPos); + BaseStorage.Read(entry.PhysicalOffset + entryPos, destination.Slice(outPos, bytesToRead)); outPos += bytesToRead; inPos += bytesToRead; @@ -72,11 +74,13 @@ namespace LibHac.Fs.Save entry = entry.Next; } } + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { - if (source.Length == 0) return; + if (source.Length == 0) return Result.Success; MapEntry entry = GetMapEntry(offset); @@ -89,7 +93,9 @@ namespace LibHac.Fs.Save long entryPos = inPos - entry.VirtualOffset; int bytesToWrite = (int)Math.Min(entry.VirtualOffsetEnd - inPos, remaining); - BaseStorage.Write(source.Slice(outPos, bytesToWrite), entry.PhysicalOffset + entryPos); + + Result rc = BaseStorage.Write(entry.PhysicalOffset + entryPos, source.Slice(outPos, bytesToWrite)); + if (rc.IsFailure()) return rc; outPos += bytesToWrite; inPos += bytesToWrite; @@ -100,14 +106,37 @@ namespace LibHac.Fs.Save entry = entry.Next; } } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { - BaseStorage.Flush(); + return BaseStorage.Flush(); } - public override long GetSize() => -1; + protected override Result SetSizeImpl(long size) + { + return ResultFs.UnsupportedOperationInHierarchicalIvfcStorageSetSize.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + // todo: Different result code + size = -1; + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!LeaveOpen) + { + BaseStorage?.Dispose(); + } + } + } public IStorage GetBaseStorage() => BaseStorage.AsReadOnly(); public IStorage GetHeaderStorage() => HeaderStorage.AsReadOnly(); @@ -199,7 +228,7 @@ namespace LibHac.Fs.Save public class RemapHeader { public string Magic { get; } - public uint Verison { get; } + public uint Version { get; } public int MapEntryCount { get; } public int MapSegmentCount { get; } public int SegmentBits { get; } @@ -209,7 +238,7 @@ namespace LibHac.Fs.Save var reader = new BinaryReader(storage.AsStream()); Magic = reader.ReadAscii(4); - Verison = reader.ReadUInt32(); + Version = reader.ReadUInt32(); MapEntryCount = reader.ReadInt32(); MapSegmentCount = reader.ReadInt32(); SegmentBits = reader.ReadInt32(); diff --git a/src/LibHac/FsSystem/Save/SaveDataDirectory.cs b/src/LibHac/FsSystem/Save/SaveDataDirectory.cs new file mode 100644 index 00000000..a1a4e061 --- /dev/null +++ b/src/LibHac/FsSystem/Save/SaveDataDirectory.cs @@ -0,0 +1,88 @@ +using System; +using System.Text; +using LibHac.Common; +using LibHac.Fs; + +namespace LibHac.FsSystem.Save +{ + public class SaveDataDirectory : IDirectory + { + private SaveDataFileSystemCore ParentFileSystem { get; } + + private OpenDirectoryMode Mode { get; } + + private SaveFindPosition InitialPosition { get; } + private SaveFindPosition _currentPosition; + + public SaveDataDirectory(SaveDataFileSystemCore fs, SaveFindPosition position, OpenDirectoryMode mode) + { + ParentFileSystem = fs; + InitialPosition = position; + _currentPosition = position; + Mode = mode; + } + + public Result Read(out long entriesRead, Span entryBuffer) + { + return ReadImpl(out entriesRead, ref _currentPosition, entryBuffer); + } + + public Result GetEntryCount(out long entryCount) + { + SaveFindPosition position = InitialPosition; + + return ReadImpl(out entryCount, ref position, Span.Empty); + } + + private Result ReadImpl(out long entriesRead, ref SaveFindPosition position, Span entryBuffer) + { + HierarchicalSaveFileTable tab = ParentFileSystem.FileTable; + + int i = 0; + + if (Mode.HasFlag(OpenDirectoryMode.Directory)) + { + while ((entryBuffer.IsEmpty || i < entryBuffer.Length) && tab.FindNextDirectory(ref position, out string name)) + { + if (!entryBuffer.IsEmpty) + { + ref DirectoryEntry entry = ref entryBuffer[i]; + Span nameUtf8 = Encoding.UTF8.GetBytes(name); + + StringUtils.Copy(entry.Name, nameUtf8); + entry.Name[64] = 0; + + entry.Type = DirectoryEntryType.Directory; + entry.Size = 0; + } + + i++; + } + } + + if (Mode.HasFlag(OpenDirectoryMode.File)) + { + while ((entryBuffer.IsEmpty || i < entryBuffer.Length) && tab.FindNextFile(ref position, out SaveFileInfo info, out string name)) + { + if (!entryBuffer.IsEmpty) + { + ref DirectoryEntry entry = ref entryBuffer[i]; + Span nameUtf8 = Encoding.UTF8.GetBytes(name); + + StringUtils.Copy(entry.Name, nameUtf8); + entry.Name[64] = 0; + + entry.Type = DirectoryEntryType.File; + entry.Size = info.Length; + } + + i++; + } + } + + entriesRead = i; + + return Result.Success; + } + } +} diff --git a/src/LibHac/FsSystem/Save/SaveDataFile.cs b/src/LibHac/FsSystem/Save/SaveDataFile.cs new file mode 100644 index 00000000..6310bff1 --- /dev/null +++ b/src/LibHac/FsSystem/Save/SaveDataFile.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using LibHac.Fs; + +namespace LibHac.FsSystem.Save +{ + public class SaveDataFile : FileBase + { + private AllocationTableStorage BaseStorage { get; } + private string Path { get; } + private HierarchicalSaveFileTable FileTable { get; } + private long Size { get; set; } + private OpenMode Mode { get; } + + public SaveDataFile(AllocationTableStorage baseStorage, string path, HierarchicalSaveFileTable fileTable, long size, OpenMode mode) + { + Mode = mode; + BaseStorage = baseStorage; + Path = path; + FileTable = fileTable; + Size = size; + } + + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) + { + bytesRead = default; + + Result rc = ValidateReadParams(out long toRead, offset, destination.Length, Mode); + if (rc.IsFailure()) return rc; + + if (toRead == 0) + { + bytesRead = 0; + return Result.Success; + } + + rc = BaseStorage.Read(offset, destination.Slice(0, (int)toRead)); + if (rc.IsFailure()) return rc; + + bytesRead = toRead; + return Result.Success; + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) + { + Result rc = ValidateWriteParams(offset, source.Length, Mode, out bool isResizeNeeded); + if (rc.IsFailure()) return rc; + + if (isResizeNeeded) + { + rc = SetSizeImpl(offset + source.Length); + if (rc.IsFailure()) return rc; + } + + BaseStorage.Write(offset, source); + + if ((options & WriteOption.Flush) != 0) + { + return Flush(); + } + + return Result.Success; + } + + protected override Result FlushImpl() + { + return BaseStorage.Flush(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Size; + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + if (size < 0) throw new ArgumentOutOfRangeException(nameof(size)); + if (Size == size) return Result.Success; + + Result rc = BaseStorage.SetSize(size); + if (rc.IsFailure()) return rc; + + if (!FileTable.TryOpenFile(Path, out SaveFileInfo fileInfo)) + { + throw new FileNotFoundException(); + } + + fileInfo.StartBlock = BaseStorage.InitialBlock; + fileInfo.Length = size; + + FileTable.AddFile(Path, ref fileInfo); + + Size = size; + + return Result.Success; + } + } +} diff --git a/src/LibHac/Fs/Save/SaveDataFileSystem.cs b/src/LibHac/FsSystem/Save/SaveDataFileSystem.cs similarity index 62% rename from src/LibHac/Fs/Save/SaveDataFileSystem.cs rename to src/LibHac/FsSystem/Save/SaveDataFileSystem.cs index 8191ccba..e0a01582 100644 --- a/src/LibHac/Fs/Save/SaveDataFileSystem.cs +++ b/src/LibHac/FsSystem/Save/SaveDataFileSystem.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { - public class SaveDataFileSystem : IFileSystem + public class SaveDataFileSystem : FileSystemBase { internal const byte TrimFillValue = 0; @@ -142,198 +142,105 @@ namespace LibHac.Fs.Save IntegrityStorageType.Save, integrityCheckLevel, LeaveOpen); } - public void CreateDirectory(string path) + protected override Result CreateDirectoryImpl(string path) { - try - { - SaveDataFileSystemCore.CreateDirectory(path); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.CreateDirectory(path); + + return SaveResults.ConvertToExternalResult(result); } - public void CreateFile(string path, long size, CreateFileOptions options) + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) { - try - { - SaveDataFileSystemCore.CreateFile(path, size, options); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.CreateFile(path, size, options); + + return SaveResults.ConvertToExternalResult(result); } - public void DeleteDirectory(string path) + protected override Result DeleteDirectoryImpl(string path) { - try - { - SaveDataFileSystemCore.DeleteDirectory(path); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.DeleteDirectory(path); + + return SaveResults.ConvertToExternalResult(result); } - public void DeleteDirectoryRecursively(string path) + protected override Result DeleteDirectoryRecursivelyImpl(string path) { - try - { - SaveDataFileSystemCore.DeleteDirectoryRecursively(path); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.DeleteDirectoryRecursively(path); + + return SaveResults.ConvertToExternalResult(result); } - public void CleanDirectoryRecursively(string path) + protected override Result CleanDirectoryRecursivelyImpl(string path) { - try - { - SaveDataFileSystemCore.CleanDirectoryRecursively(path); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.CleanDirectoryRecursively(path); + + return SaveResults.ConvertToExternalResult(result); } - public void DeleteFile(string path) + protected override Result DeleteFileImpl(string path) { - try - { - SaveDataFileSystemCore.DeleteFile(path); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.DeleteFile(path); + + return SaveResults.ConvertToExternalResult(result); } - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) { - try - { - return SaveDataFileSystemCore.OpenDirectory(path, mode); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.OpenDirectory(out directory, path, mode); + + return SaveResults.ConvertToExternalResult(result); } - public IFile OpenFile(string path, OpenMode mode) + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) { - try - { - return SaveDataFileSystemCore.OpenFile(path, mode); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.OpenFile(out file, path, mode); + + return SaveResults.ConvertToExternalResult(result); } - public void RenameDirectory(string srcPath, string dstPath) + protected override Result RenameDirectoryImpl(string oldPath, string newPath) { - try - { - SaveDataFileSystemCore.RenameDirectory(srcPath, dstPath); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.RenameDirectory(oldPath, newPath); + + return SaveResults.ConvertToExternalResult(result); } - public void RenameFile(string srcPath, string dstPath) + protected override Result RenameFileImpl(string oldPath, string newPath) { - try - { - SaveDataFileSystemCore.RenameFile(srcPath, dstPath); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.RenameFile(oldPath, newPath); + + return SaveResults.ConvertToExternalResult(result); } - public DirectoryEntryType GetEntryType(string path) + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) { - try - { - return SaveDataFileSystemCore.GetEntryType(path); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.GetEntryType(out entryType, path); + + return SaveResults.ConvertToExternalResult(result); } - public long GetFreeSpaceSize(string path) + protected override Result GetFreeSpaceSizeImpl(out long freeSpace, string path) { - try - { - return SaveDataFileSystemCore.GetFreeSpaceSize(path); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.GetFreeSpaceSize(out freeSpace, path); + + return SaveResults.ConvertToExternalResult(result); } - public long GetTotalSpaceSize(string path) + protected override Result GetTotalSpaceSizeImpl(out long totalSpace, string path) { - try - { - return SaveDataFileSystemCore.GetTotalSpaceSize(path); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = SaveDataFileSystemCore.GetTotalSpaceSize(out totalSpace, path); + + return SaveResults.ConvertToExternalResult(result); } - public void Commit() + protected override Result CommitImpl() { - try - { - Commit(Keyset); - } - catch (HorizonResultException ex) - { - ConvertResultException(ex); - throw; - } + Result result = Commit(Keyset); + + return SaveResults.ConvertToExternalResult(result); } - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) => - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - - public bool Commit(Keyset keyset) + public Result Commit(Keyset keyset) { CoreDataIvfcStorage.Flush(); FatIvfcStorage?.Flush(); @@ -349,7 +256,7 @@ namespace LibHac.Fs.Save headerStream.Position = 0x108; headerStream.Write(hash, 0, hash.Length); - if (keyset == null || keyset.SaveMacKey.IsEmpty()) return false; + if (keyset == null || keyset.SaveMacKey.IsEmpty()) return ResultFs.PreconditionViolation; var cmacData = new byte[0x200]; var cmac = new byte[0x10]; @@ -363,7 +270,7 @@ namespace LibHac.Fs.Save headerStream.Write(cmac, 0, 0x10); headerStream.Flush(); - return true; + return Result.Success; } public void FsTrim() @@ -395,10 +302,5 @@ namespace LibHac.Fs.Save return journalValidity; } - - private void ConvertResultException(HorizonResultException ex) - { - ex.ResultValue = SaveResults.ConvertToExternalResult(ex.ResultValue); - } } } diff --git a/src/LibHac/Fs/Save/SaveDataFileSystemCore.cs b/src/LibHac/FsSystem/Save/SaveDataFileSystemCore.cs similarity index 60% rename from src/LibHac/Fs/Save/SaveDataFileSystemCore.cs rename to src/LibHac/FsSystem/Save/SaveDataFileSystemCore.cs index a1fe5c3c..5b8fd880 100644 --- a/src/LibHac/Fs/Save/SaveDataFileSystemCore.cs +++ b/src/LibHac/FsSystem/Save/SaveDataFileSystemCore.cs @@ -1,9 +1,9 @@ -using System; -using System.IO; +using System.IO; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { - public class SaveDataFileSystemCore : IFileSystem + public class SaveDataFileSystemCore : FileSystemBase { private IStorage BaseStorage { get; } private IStorage HeaderStorage { get; } @@ -27,14 +27,16 @@ namespace LibHac.Fs.Save FileTable = new HierarchicalSaveFileTable(dirTableStorage, fileTableStorage); } - public void CreateDirectory(string path) + protected override Result CreateDirectoryImpl(string path) { path = PathTools.Normalize(path); FileTable.AddDirectory(path); + + return Result.Success; } - public void CreateFile(string path, long size, CreateFileOptions options) + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) { path = PathTools.Normalize(path); @@ -43,7 +45,7 @@ namespace LibHac.Fs.Save var emptyFileEntry = new SaveFileInfo { StartBlock = int.MinValue, Length = size }; FileTable.AddFile(path, ref emptyFileEntry); - return; + return Result.Success; } int blockCount = (int)Util.DivideByRoundUp(size, AllocationTable.Header.BlockSize); @@ -51,45 +53,53 @@ namespace LibHac.Fs.Save if (startBlock == -1) { - ThrowHelper.ThrowResult(ResultFs.AllocationTableInsufficientFreeBlocks, - "Not enough available space to create file."); + return ResultFs.AllocationTableInsufficientFreeBlocks.Log(); } var fileEntry = new SaveFileInfo { StartBlock = startBlock, Length = size }; FileTable.AddFile(path, ref fileEntry); + + return Result.Success; } - public void DeleteDirectory(string path) + protected override Result DeleteDirectoryImpl(string path) { path = PathTools.Normalize(path); FileTable.DeleteDirectory(path); + + return Result.Success; } - public void DeleteDirectoryRecursively(string path) + protected override Result DeleteDirectoryRecursivelyImpl(string path) { path = PathTools.Normalize(path); - CleanDirectoryRecursively(path); + Result rc = CleanDirectoryRecursively(path); + if (rc.IsFailure()) return rc; + DeleteDirectory(path); + + return Result.Success; } - public void CleanDirectoryRecursively(string path) + protected override Result CleanDirectoryRecursivelyImpl(string path) { path = PathTools.Normalize(path); - IDirectory dir = OpenDirectory(path, OpenDirectoryMode.All); - FileSystemExtensions.CleanDirectoryRecursivelyGeneric(dir); + FileSystemExtensions.CleanDirectoryRecursivelyGeneric(this, path); + + return Result.Success; } - public void DeleteFile(string path) + protected override Result DeleteFileImpl(string path) { path = PathTools.Normalize(path); if (!FileTable.TryOpenFile(path, out SaveFileInfo fileInfo)) { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); + return ResultFs.PathNotFound.Log(); } if (fileInfo.StartBlock != int.MinValue) @@ -98,92 +108,100 @@ namespace LibHac.Fs.Save } FileTable.DeleteFile(path); + + return Result.Success; } - public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) { + directory = default; path = PathTools.Normalize(path); if (!FileTable.TryOpenDirectory(path, out SaveFindPosition position)) { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); + return ResultFs.PathNotFound.Log(); } - return new SaveDataDirectory(this, path, position, mode); + directory = new SaveDataDirectory(this, position, mode); + + return Result.Success; } - public IFile OpenFile(string path, OpenMode mode) + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) { + file = default; path = PathTools.Normalize(path); - if (!FileTable.TryOpenFile(path, out SaveFileInfo file)) + if (!FileTable.TryOpenFile(path, out SaveFileInfo fileInfo)) { - ThrowHelper.ThrowResult(ResultFs.PathNotFound); + return ResultFs.PathNotFound.Log(); } - AllocationTableStorage storage = OpenFatStorage(file.StartBlock); + AllocationTableStorage storage = OpenFatStorage(fileInfo.StartBlock); - return new SaveDataFile(storage, path, FileTable, file.Length, mode); + file = new SaveDataFile(storage, path, FileTable, fileInfo.Length, mode); + + return Result.Success; } - public void RenameDirectory(string srcPath, string dstPath) + protected override Result RenameDirectoryImpl(string oldPath, string newPath) { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); + oldPath = PathTools.Normalize(oldPath); + newPath = PathTools.Normalize(newPath); - FileTable.RenameDirectory(srcPath, dstPath); + return FileTable.RenameDirectory(oldPath, newPath); } - public void RenameFile(string srcPath, string dstPath) + protected override Result RenameFileImpl(string oldPath, string newPath) { - srcPath = PathTools.Normalize(srcPath); - dstPath = PathTools.Normalize(dstPath); + oldPath = PathTools.Normalize(oldPath); + newPath = PathTools.Normalize(newPath); - FileTable.RenameFile(srcPath, dstPath); + FileTable.RenameFile(oldPath, newPath); + + return Result.Success; } - public DirectoryEntryType GetEntryType(string path) + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) { path = PathTools.Normalize(path); if (FileTable.TryOpenFile(path, out SaveFileInfo _)) { - return DirectoryEntryType.File; + entryType = DirectoryEntryType.File; + return Result.Success; } if (FileTable.TryOpenDirectory(path, out SaveFindPosition _)) { - return DirectoryEntryType.Directory; + entryType = DirectoryEntryType.Directory; + return Result.Success; } - return DirectoryEntryType.NotFound; + entryType = DirectoryEntryType.NotFound; + return ResultFs.PathNotFound.Log(); } - public long GetFreeSpaceSize(string path) + protected override Result GetFreeSpaceSizeImpl(out long freeSpace, string path) { int freeBlockCount = AllocationTable.GetFreeListLength(); - return Header.BlockSize * freeBlockCount; + freeSpace = Header.BlockSize * freeBlockCount; + + return Result.Success; } - public long GetTotalSpaceSize(string path) + protected override Result GetTotalSpaceSizeImpl(out long totalSpace, string path) { - return Header.BlockSize * Header.BlockCount; + totalSpace = Header.BlockSize * Header.BlockCount; + + return Result.Success; } - public void Commit() + protected override Result CommitImpl() { - + return Result.Success; } - public FileTimeStampRaw GetFileTimeStampRaw(string path) - { - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - return default; - } - - public void QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, string path, QueryId queryId) => - ThrowHelper.ThrowResult(ResultFs.NotImplemented); - public IStorage GetBaseStorage() => BaseStorage.AsReadOnly(); public IStorage GetHeaderStorage() => HeaderStorage.AsReadOnly(); @@ -191,7 +209,7 @@ namespace LibHac.Fs.Save { AllocationTable.FsTrim(); - foreach (DirectoryEntry file in this.EnumerateEntries("*", SearchOptions.RecurseSubdirectories)) + foreach (DirectoryEntryEx file in this.EnumerateEntries("*", SearchOptions.RecurseSubdirectories)) { if (FileTable.TryOpenFile(file.FullPath, out SaveFileInfo fileInfo) && fileInfo.StartBlock >= 0) { diff --git a/src/LibHac/Fs/Save/SaveExtensions.cs b/src/LibHac/FsSystem/Save/SaveExtensions.cs similarity index 93% rename from src/LibHac/Fs/Save/SaveExtensions.cs rename to src/LibHac/FsSystem/Save/SaveExtensions.cs index 6c1ed069..0507f229 100644 --- a/src/LibHac/Fs/Save/SaveExtensions.cs +++ b/src/LibHac/FsSystem/Save/SaveExtensions.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { public static class SaveExtensions { diff --git a/src/LibHac/Fs/Save/SaveFsEntry.cs b/src/LibHac/FsSystem/Save/SaveFsEntry.cs similarity index 96% rename from src/LibHac/Fs/Save/SaveFsEntry.cs rename to src/LibHac/FsSystem/Save/SaveFsEntry.cs index 1a4c14a7..49e01214 100644 --- a/src/LibHac/Fs/Save/SaveFsEntry.cs +++ b/src/LibHac/FsSystem/Save/SaveFsEntry.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { internal ref struct SaveEntryKey { diff --git a/src/LibHac/Fs/Save/SaveFsList.cs b/src/LibHac/FsSystem/Save/SaveFsList.cs similarity index 96% rename from src/LibHac/Fs/Save/SaveFsList.cs rename to src/LibHac/FsSystem/Save/SaveFsList.cs index 0bf9d428..2d83c572 100644 --- a/src/LibHac/Fs/Save/SaveFsList.cs +++ b/src/LibHac/FsSystem/Save/SaveFsList.cs @@ -3,8 +3,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using LibHac.Fs; -namespace LibHac.Fs.Save +namespace LibHac.FsSystem.Save { // todo: Change constraint to "unmanaged" after updating to // a newer SDK https://github.com/dotnet/csharplang/issues/1937 @@ -101,10 +102,10 @@ namespace LibHac.Fs.Save if (capacity == 0 || length >= capacity) { - long currentSize = Storage.GetSize(); + Storage.GetSize(out long currentSize).ThrowIfFailure(); Storage.SetSize(currentSize + CapacityIncrement); - long newSize = Storage.GetSize(); + Storage.GetSize(out long newSize).ThrowIfFailure(); SetListCapacity((int)(newSize / _sizeOfEntry)); } @@ -282,7 +283,7 @@ namespace LibHac.Fs.Save private int GetListCapacity() { Span buf = stackalloc byte[sizeof(int)]; - Storage.Read(buf, 4); + Storage.Read(4, buf).ThrowIfFailure(); return MemoryMarshal.Read(buf); } @@ -290,7 +291,7 @@ namespace LibHac.Fs.Save private int GetListLength() { Span buf = stackalloc byte[sizeof(int)]; - Storage.Read(buf, 0); + Storage.Read(0, buf).ThrowIfFailure(); return MemoryMarshal.Read(buf); } @@ -300,7 +301,7 @@ namespace LibHac.Fs.Save Span buf = stackalloc byte[sizeof(int)]; MemoryMarshal.Write(buf, ref capacity); - Storage.Write(buf, 4); + Storage.Write(4, buf).ThrowIfFailure(); } private void SetListLength(int length) @@ -308,7 +309,7 @@ namespace LibHac.Fs.Save Span buf = stackalloc byte[sizeof(int)]; MemoryMarshal.Write(buf, ref length); - Storage.Write(buf, 0); + Storage.Write(0, buf).ThrowIfFailure(); } private void ReadEntry(int index, out SaveFsEntry entry) @@ -352,7 +353,7 @@ namespace LibHac.Fs.Save Debug.Assert(entry.Length == _sizeOfEntry); int offset = index * _sizeOfEntry; - Storage.Read(entry, offset); + Storage.Read(offset, entry); } private void WriteEntry(int index, Span entry) @@ -360,7 +361,7 @@ namespace LibHac.Fs.Save Debug.Assert(entry.Length == _sizeOfEntry); int offset = index * _sizeOfEntry; - Storage.Write(entry, offset); + Storage.Write(offset, entry); } private ref SaveFsEntry GetEntryFromBytes(Span entry) diff --git a/src/LibHac/Fs/Save/SaveResults.cs b/src/LibHac/FsSystem/Save/SaveResults.cs similarity index 98% rename from src/LibHac/Fs/Save/SaveResults.cs rename to src/LibHac/FsSystem/Save/SaveResults.cs index 3935db9e..c8bfdc9a 100644 --- a/src/LibHac/Fs/Save/SaveResults.cs +++ b/src/LibHac/FsSystem/Save/SaveResults.cs @@ -1,4 +1,6 @@ -namespace LibHac.Fs.Save +using LibHac.Fs; + +namespace LibHac.FsSystem.Save { internal static class SaveResults { diff --git a/src/LibHac/FsSystem/SectorStorage.cs b/src/LibHac/FsSystem/SectorStorage.cs new file mode 100644 index 00000000..6e0a09bc --- /dev/null +++ b/src/LibHac/FsSystem/SectorStorage.cs @@ -0,0 +1,90 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class SectorStorage : StorageBase + { + protected IStorage BaseStorage { get; } + + public int SectorSize { get; } + public int SectorCount { get; private set; } + + private long Length { get; set; } + private bool LeaveOpen { get; } + + public SectorStorage(IStorage baseStorage, int sectorSize, bool leaveOpen) + { + BaseStorage = baseStorage; + SectorSize = sectorSize; + + baseStorage.GetSize(out long baseSize).ThrowIfFailure(); + + SectorCount = (int)Util.DivideByRoundUp(baseSize, SectorSize); + Length = baseSize; + + LeaveOpen = leaveOpen; + } + + protected override Result ReadImpl(long offset, Span destination) + { + ValidateSize(destination.Length, offset); + return BaseStorage.Read(offset, destination); + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source) + { + ValidateSize(source.Length, offset); + return BaseStorage.Write(offset, source); + } + + protected override Result FlushImpl() + { + return BaseStorage.Flush(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + Result rc = BaseStorage.SetSize(size); + if (rc.IsFailure()) return rc; + + rc = BaseStorage.GetSize(out long newSize); + if (rc.IsFailure()) return rc; + + SectorCount = (int)Util.DivideByRoundUp(newSize, SectorSize); + Length = newSize; + + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!LeaveOpen) + { + BaseStorage?.Dispose(); + } + } + } + + /// + /// Validates that the size is a multiple of the sector size + /// + protected void ValidateSize(long size, long offset) + { + if (size < 0) + throw new ArgumentException("Size must be non-negative"); + if (offset < 0) + throw new ArgumentException("Offset must be non-negative"); + if (offset % SectorSize != 0) + throw new ArgumentException($"Offset must be a multiple of {SectorSize}"); + } + } +} diff --git a/src/LibHac/Fs/StorageExtensions.cs b/src/LibHac/FsSystem/StorageExtensions.cs similarity index 80% rename from src/LibHac/Fs/StorageExtensions.cs rename to src/LibHac/FsSystem/StorageExtensions.cs index 16340ecf..3deebdf5 100644 --- a/src/LibHac/Fs/StorageExtensions.cs +++ b/src/LibHac/FsSystem/StorageExtensions.cs @@ -2,20 +2,22 @@ using System.Buffers; using System.IO; using System.Runtime.InteropServices; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public static class StorageExtensions { - public static void Read(this IStorage storage, byte[] buffer, long offset, int count, int bufferOffset) + public static Result Read(this IStorage storage, long offset, byte[] buffer, int count, int bufferOffset) { ValidateStorageParameters(buffer, offset, count, bufferOffset); - storage.Read(buffer.AsSpan(bufferOffset, count), offset); + return storage.Read(offset, buffer.AsSpan(bufferOffset, count)); } - public static void Write(this IStorage storage, byte[] buffer, long offset, int count, int bufferOffset) + + public static Result Write(this IStorage storage, long offset, byte[] buffer, int count, int bufferOffset) { ValidateStorageParameters(buffer, offset, count, bufferOffset); - storage.Write(buffer.AsSpan(bufferOffset, count), offset); + return storage.Write(offset, buffer.AsSpan(bufferOffset, count)); } private static void ValidateStorageParameters(byte[] buffer, long offset, int count, int bufferOffset) @@ -28,7 +30,7 @@ namespace LibHac.Fs public static IStorage Slice(this IStorage storage, long start) { - long length = storage.GetSize(); + storage.GetSize(out long length).ThrowIfFailure(); if (length == -1) { @@ -53,10 +55,10 @@ namespace LibHac.Fs return storage.AsReadOnly(true); } - // Todo: Move out of SubStorage public static IStorage AsReadOnly(this IStorage storage, bool leaveOpen) { - return new SubStorage(storage, 0, storage.GetSize(), leaveOpen, FileAccess.Read); + storage.GetSize(out long storageSize).ThrowIfFailure(); + return new SubStorage(storage, 0, storageSize, leaveOpen, FileAccess.Read); } public static Stream AsStream(this IStorage storage) => new StorageStream(storage, FileAccess.ReadWrite, true); @@ -68,7 +70,11 @@ namespace LibHac.Fs public static void CopyTo(this IStorage input, IStorage output, IProgressReport progress = null) { const int bufferSize = 81920; - long remaining = Math.Min(input.GetSize(), output.GetSize()); + + input.GetSize(out long inputSize).ThrowIfFailure(); + output.GetSize(out long outputSize).ThrowIfFailure(); + + long remaining = Math.Min(inputSize, outputSize); if (remaining < 0) throw new ArgumentException("Storage must have an explicit length"); progress?.SetTotal(remaining); @@ -81,8 +87,8 @@ namespace LibHac.Fs { int toCopy = (int)Math.Min(bufferSize, remaining); Span buf = buffer.AsSpan(0, toCopy); - input.Read(buf, pos); - output.Write(buf, pos); + input.Read(pos, buf); + output.Write(pos, buf); remaining -= toCopy; pos += toCopy; @@ -100,7 +106,8 @@ namespace LibHac.Fs public static void Fill(this IStorage input, byte value, IProgressReport progress = null) { - input.Fill(value, 0, input.GetSize(), progress); + input.GetSize(out long inputSize).ThrowIfFailure(); + input.Fill(value, 0, inputSize, progress); } public static void Fill(this IStorage input, byte value, long offset, long count, IProgressReport progress = null) @@ -116,7 +123,7 @@ namespace LibHac.Fs Span buf = stackalloc byte[(int)count]; buf.Fill(value); - input.Write(buf, offset); + input.Write(offset, buf); } private static void FillLarge(this IStorage input, byte value, long offset, long count, IProgressReport progress = null) @@ -139,7 +146,7 @@ namespace LibHac.Fs int toFill = (int)Math.Min(bufferSize, remaining); Span buf = buffer.AsSpan(0, toFill); - input.Write(buf, pos); + input.Write(pos, buf); remaining -= toFill; pos += toFill; @@ -157,9 +164,11 @@ namespace LibHac.Fs public static void WriteAllBytes(this IStorage input, string filename, IProgressReport progress = null) { + input.GetSize(out long inputSize).ThrowIfFailure(); + using (var outFile = new FileStream(filename, FileMode.Create, FileAccess.Write)) { - input.CopyToStream(outFile, input.GetSize(), progress); + input.CopyToStream(outFile, inputSize, progress); } } @@ -167,7 +176,9 @@ namespace LibHac.Fs { if (storage == null) return new byte[0]; - var arr = new byte[storage.GetSize()]; + storage.GetSize(out long storageSize).ThrowIfFailure(); + + var arr = new byte[storageSize]; storage.CopyTo(new MemoryStorage(arr)); return arr; } @@ -176,10 +187,12 @@ namespace LibHac.Fs { if (storage == null) return new T[0]; - var arr = new T[storage.GetSize() / Marshal.SizeOf()]; + storage.GetSize(out long storageSize).ThrowIfFailure(); + + var arr = new T[storageSize / Marshal.SizeOf()]; Span dest = MemoryMarshal.Cast(arr.AsSpan()); - storage.Read(dest, 0); + storage.Read(0, dest); return arr; } @@ -194,7 +207,7 @@ namespace LibHac.Fs while (remaining > 0) { int toWrite = (int)Math.Min(buffer.Length, remaining); - input.Read(buffer.AsSpan(0, toWrite), inOffset); + input.Read(inOffset, buffer.AsSpan(0, toWrite)); output.Write(buffer, 0, toWrite); remaining -= toWrite; @@ -203,7 +216,11 @@ namespace LibHac.Fs } } - public static void CopyToStream(this IStorage input, Stream output) => CopyToStream(input, output, input.GetSize()); + public static void CopyToStream(this IStorage input, Stream output) + { + input.GetSize(out long inputSize).ThrowIfFailure(); + CopyToStream(input, output, inputSize); + } public static IStorage AsStorage(this Stream stream) { diff --git a/src/LibHac/FsSystem/StorageFile.cs b/src/LibHac/FsSystem/StorageFile.cs new file mode 100644 index 00000000..96d23a30 --- /dev/null +++ b/src/LibHac/FsSystem/StorageFile.cs @@ -0,0 +1,80 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class StorageFile : FileBase + { + private IStorage BaseStorage { get; } + private OpenMode Mode { get; } + + public StorageFile(IStorage baseStorage, OpenMode mode) + { + BaseStorage = baseStorage; + Mode = mode; + } + + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) + { + bytesRead = default; + + Result rc = ValidateReadParams(out long toRead, offset, destination.Length, Mode); + if (rc.IsFailure()) return rc; + + if (toRead == 0) + { + bytesRead = 0; + return Result.Success; + } + + rc = BaseStorage.Read(offset, destination.Slice(0, (int)toRead)); + if (rc.IsFailure()) return rc; + + bytesRead = toRead; + return Result.Success; + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) + { + Result rc = ValidateWriteParams(offset, source.Length, Mode, out bool isResizeNeeded); + if (rc.IsFailure()) return rc; + + if (isResizeNeeded) + { + rc = SetSizeImpl(offset + source.Length); + if (rc.IsFailure()) return rc; + } + + rc = BaseStorage.Write(offset, source); + if (rc.IsFailure()) return rc; + + if (options.HasFlag(WriteOption.Flush)) + { + return Flush(); + } + + return Result.Success; + } + + protected override Result FlushImpl() + { + if (!Mode.HasFlag(OpenMode.Write)) + return Result.Success; + + return BaseStorage.Flush(); + } + + protected override Result GetSizeImpl(out long size) + { + return BaseStorage.GetSize(out size); + } + + protected override Result SetSizeImpl(long size) + { + if (!Mode.HasFlag(OpenMode.Write)) + return ResultFs.InvalidOpenModeForWrite.Log(); + + return BaseStorage.SetSize(size); + } + } +} diff --git a/src/LibHac/Fs/StorageStream.cs b/src/LibHac/FsSystem/StorageStream.cs similarity index 82% rename from src/LibHac/Fs/StorageStream.cs rename to src/LibHac/FsSystem/StorageStream.cs index bbd3d6e9..0f900f70 100644 --- a/src/LibHac/Fs/StorageStream.cs +++ b/src/LibHac/FsSystem/StorageStream.cs @@ -1,7 +1,8 @@ using System; using System.IO; +using LibHac.Fs; -namespace LibHac.Fs +namespace LibHac.FsSystem { public class StorageStream : Stream { @@ -13,7 +14,8 @@ namespace LibHac.Fs { BaseStorage = baseStorage; LeaveOpen = leaveOpen; - _length = baseStorage.GetSize(); + + baseStorage.GetSize(out _length).ThrowIfFailure(); CanRead = access.HasFlag(FileAccess.Read); CanWrite = access.HasFlag(FileAccess.Write); @@ -22,7 +24,7 @@ namespace LibHac.Fs public override int Read(byte[] buffer, int offset, int count) { int toRead = (int)Math.Min(count, Length - Position); - BaseStorage.Read(buffer.AsSpan(offset, toRead), Position); + BaseStorage.Read(Position, buffer.AsSpan(offset, toRead)).ThrowIfFailure(); Position += toRead; return toRead; @@ -30,7 +32,7 @@ namespace LibHac.Fs public override void Write(byte[] buffer, int offset, int count) { - BaseStorage.Write(buffer.AsSpan(offset, count), Position); + BaseStorage.Write(Position, buffer.AsSpan(offset, count)).ThrowIfFailure(); Position += count; } @@ -59,9 +61,9 @@ namespace LibHac.Fs public override void SetLength(long value) { - BaseStorage.SetSize(value); + BaseStorage.SetSize(value).ThrowIfFailure(); - _length = BaseStorage.GetSize(); + BaseStorage.GetSize(out _length).ThrowIfFailure(); } public override bool CanRead { get; } diff --git a/src/LibHac/Fs/StreamFile.cs b/src/LibHac/FsSystem/StreamFile.cs similarity index 57% rename from src/LibHac/Fs/StreamFile.cs rename to src/LibHac/FsSystem/StreamFile.cs index e81b642f..5114af65 100644 --- a/src/LibHac/Fs/StreamFile.cs +++ b/src/LibHac/FsSystem/StreamFile.cs @@ -1,17 +1,21 @@ using System; using System.IO; +using LibHac.Fs; #if !STREAM_SPAN using System.Buffers; #endif -namespace LibHac.Fs +namespace LibHac.FsSystem { /// /// Provides an interface for interacting with a /// public class StreamFile : FileBase { + // todo: handle Stream exceptions + + private OpenMode Mode { get; } private Stream BaseStream { get; } private object Locker { get; } = new object(); @@ -21,8 +25,13 @@ namespace LibHac.Fs Mode = mode; } - public override int Read(Span destination, long offset, ReadOption options) + protected override Result ReadImpl(out long bytesRead, long offset, Span destination, ReadOption options) { + bytesRead = default; + + Result rc = ValidateReadParams(out long toRead, offset, destination.Length, Mode); + if (rc.IsFailure()) return rc; + #if STREAM_SPAN lock (Locker) { @@ -31,13 +40,13 @@ namespace LibHac.Fs BaseStream.Position = offset; } - return BaseStream.Read(destination); + bytesRead = BaseStream.Read(destination.Slice(0, (int)toRead)); + return Result.Success; } #else - byte[] buffer = ArrayPool.Shared.Rent(destination.Length); + byte[] buffer = ArrayPool.Shared.Rent((int)toRead); try { - int bytesRead; lock (Locker) { if (BaseStream.Position != offset) @@ -45,19 +54,22 @@ namespace LibHac.Fs BaseStream.Position = offset; } - bytesRead = BaseStream.Read(buffer, 0, destination.Length); + bytesRead = BaseStream.Read(buffer, 0, (int)toRead); } - new Span(buffer, 0, destination.Length).CopyTo(destination); + new Span(buffer, 0, (int)bytesRead).CopyTo(destination); - return bytesRead; + return Result.Success; } finally { ArrayPool.Shared.Return(buffer); } #endif } - public override void Write(ReadOnlySpan source, long offset, WriteOption options) + protected override Result WriteImpl(long offset, ReadOnlySpan source, WriteOption options) { + Result rc = ValidateWriteParams(offset, source.Length, Mode, out _); + if (rc.IsFailure()) return rc; + #if STREAM_SPAN lock (Locker) { @@ -79,33 +91,38 @@ namespace LibHac.Fs finally { ArrayPool.Shared.Return(buffer); } #endif - if ((options & WriteOption.Flush) != 0) + if (options.HasFlag(WriteOption.Flush)) { - Flush(); + return Flush(); } + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { lock (Locker) { BaseStream.Flush(); + return Result.Success; } } - public override long GetSize() + protected override Result GetSizeImpl(out long size) { lock (Locker) { - return BaseStream.Length; + size = BaseStream.Length; + return Result.Success; } } - public override void SetSize(long size) + protected override Result SetSizeImpl(long size) { lock (Locker) { BaseStream.SetLength(size); + return Result.Success; } } } diff --git a/src/LibHac/Fs/StreamStorage.cs b/src/LibHac/FsSystem/StreamStorage.cs similarity index 66% rename from src/LibHac/Fs/StreamStorage.cs rename to src/LibHac/FsSystem/StreamStorage.cs index 154caa51..85505e87 100644 --- a/src/LibHac/Fs/StreamStorage.cs +++ b/src/LibHac/FsSystem/StreamStorage.cs @@ -1,26 +1,30 @@ using System; using System.IO; +using LibHac.Fs; #if !STREAM_SPAN using System.Buffers; #endif -namespace LibHac.Fs +namespace LibHac.FsSystem { public class StreamStorage : StorageBase { + // todo: handle Stream exceptions + private Stream BaseStream { get; } private object Locker { get; } = new object(); - private long _length; + private long Length { get; } + private bool LeaveOpen { get; } public StreamStorage(Stream baseStream, bool leaveOpen) { BaseStream = baseStream; - _length = BaseStream.Length; - if (!leaveOpen) ToDispose.Add(BaseStream); + Length = BaseStream.Length; + LeaveOpen = leaveOpen; } - protected override void ReadImpl(Span destination, long offset) + protected override Result ReadImpl(long offset, Span destination) { #if STREAM_SPAN lock (Locker) @@ -50,9 +54,11 @@ namespace LibHac.Fs } finally { ArrayPool.Shared.Return(buffer); } #endif + + return Result.Success; } - protected override void WriteImpl(ReadOnlySpan source, long offset) + protected override Result WriteImpl(long offset, ReadOnlySpan source) { #if STREAM_SPAN lock (Locker) @@ -82,16 +88,40 @@ namespace LibHac.Fs } finally { ArrayPool.Shared.Return(buffer); } #endif + + return Result.Success; } - public override void Flush() + protected override Result FlushImpl() { lock (Locker) { BaseStream.Flush(); + + return Result.Success; } } - public override long GetSize() => _length; + protected override Result SetSizeImpl(long size) + { + return ResultFs.NotImplemented.Log(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!LeaveOpen) + { + BaseStream?.Dispose(); + } + } + } } } diff --git a/src/LibHac/FsSystem/SubStorage.cs b/src/LibHac/FsSystem/SubStorage.cs new file mode 100644 index 00000000..efaa4b7e --- /dev/null +++ b/src/LibHac/FsSystem/SubStorage.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class SubStorage : StorageBase + { + private IStorage BaseStorage { get; } + private long Offset { get; } + private FileAccess Access { get; } = FileAccess.ReadWrite; + private long Length { get; set; } + private bool LeaveOpen { get; } + + public SubStorage(IStorage baseStorage, long offset, long length) + { + BaseStorage = baseStorage; + Offset = offset; + Length = length; + } + + public SubStorage(SubStorage baseStorage, long offset, long length) + { + BaseStorage = baseStorage.BaseStorage; + Offset = baseStorage.Offset + offset; + Length = length; + } + + public SubStorage(IStorage baseStorage, long offset, long length, bool leaveOpen) + : this(baseStorage, offset, length) + { + LeaveOpen = leaveOpen; + } + + public SubStorage(IStorage baseStorage, long offset, long length, bool leaveOpen, FileAccess access) + : this(baseStorage, offset, length, leaveOpen) + { + Access = access; + } + + protected override Result ReadImpl(long offset, Span destination) + { + if ((Access & FileAccess.Read) == 0) throw new InvalidOperationException("Storage is not readable"); + return BaseStorage.Read(offset + Offset, destination); + } + + protected override Result WriteImpl(long offset, ReadOnlySpan source) + { + if ((Access & FileAccess.Write) == 0) throw new InvalidOperationException("Storage is not writable"); + return BaseStorage.Write(offset + Offset, source); + } + + protected override Result FlushImpl() + { + return BaseStorage.Flush(); + } + + protected override Result GetSizeImpl(out long size) + { + size = Length; + return Result.Success; + } + + protected override Result SetSizeImpl(long size) + { + if (BaseStorage == null) return ResultFs.SubStorageNotInitialized.Log(); + + // todo: Add IsResizable member + // if (!IsResizable) return ResultFs.SubStorageNotResizable.Log(); + + if (Offset < 0 || size < 0) return ResultFs.InvalidSize.Log(); + + Result rc = BaseStorage.GetSize(out long baseSize); + if (rc.IsFailure()) return rc; + + if (baseSize != Offset + Length) + { + // SubStorage cannot be resized unless it is located at the end of the base storage. + return ResultFs.SubStorageNotResizableMiddleOfFile.Log(); + } + + rc = BaseStorage.SetSize(Offset + size); + if (rc.IsFailure()) return rc; + + Length = size; + + return Result.Success; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!LeaveOpen) + { + BaseStorage?.Dispose(); + } + } + } + } +} diff --git a/src/LibHac/FsSystem/SubdirectoryFileSystem.cs b/src/LibHac/FsSystem/SubdirectoryFileSystem.cs new file mode 100644 index 00000000..8b00bd71 --- /dev/null +++ b/src/LibHac/FsSystem/SubdirectoryFileSystem.cs @@ -0,0 +1,134 @@ +using System; +using LibHac.Fs; + +namespace LibHac.FsSystem +{ + public class SubdirectoryFileSystem : FileSystemBase + { + private string RootPath { get; } + private IFileSystem ParentFileSystem { get; } + + private string ResolveFullPath(string path) + { + return PathTools.Combine(RootPath, path); + } + + public SubdirectoryFileSystem(IFileSystem fs, string rootPath) + { + ParentFileSystem = fs; + RootPath = PathTools.Normalize(rootPath); + } + + protected override Result CreateDirectoryImpl(string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.CreateDirectory(fullPath); + } + + protected override Result CreateFileImpl(string path, long size, CreateFileOptions options) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.CreateFile(fullPath, size, options); + } + + protected override Result DeleteDirectoryImpl(string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.DeleteDirectory(fullPath); + } + + protected override Result DeleteDirectoryRecursivelyImpl(string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.DeleteDirectoryRecursively(fullPath); + } + + protected override Result CleanDirectoryRecursivelyImpl(string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.CleanDirectoryRecursively(fullPath); + } + + protected override Result DeleteFileImpl(string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.DeleteFile(fullPath); + } + + protected override Result OpenDirectoryImpl(out IDirectory directory, string path, OpenDirectoryMode mode) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.OpenDirectory(out directory, fullPath, mode); + } + + protected override Result OpenFileImpl(out IFile file, string path, OpenMode mode) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.OpenFile(out file, fullPath, mode); + } + + protected override Result RenameDirectoryImpl(string oldPath, string newPath) + { + string fullOldPath = ResolveFullPath(PathTools.Normalize(oldPath)); + string fullNewPath = ResolveFullPath(PathTools.Normalize(newPath)); + + return ParentFileSystem.RenameDirectory(fullOldPath, fullNewPath); + } + + protected override Result RenameFileImpl(string oldPath, string newPath) + { + string fullOldPath = ResolveFullPath(PathTools.Normalize(oldPath)); + string fullNewPath = ResolveFullPath(PathTools.Normalize(newPath)); + + return ParentFileSystem.RenameFile(fullOldPath, fullNewPath); + } + + protected override Result GetEntryTypeImpl(out DirectoryEntryType entryType, string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.GetEntryType(out entryType, fullPath); + } + + protected override Result CommitImpl() + { + return ParentFileSystem.Commit(); + } + + protected override Result GetFreeSpaceSizeImpl(out long freeSpace, string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.GetFreeSpaceSize(out freeSpace, fullPath); + } + + protected override Result GetTotalSpaceSizeImpl(out long totalSpace, string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.GetTotalSpaceSize(out totalSpace, fullPath); + } + + protected override Result GetFileTimeStampRawImpl(out FileTimeStampRaw timeStamp, string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.GetFileTimeStampRaw(out timeStamp, fullPath); + } + + protected override Result QueryEntryImpl(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, string path) + { + string fullPath = ResolveFullPath(PathTools.Normalize(path)); + + return ParentFileSystem.QueryEntry(outBuffer, inBuffer, queryId, fullPath); + } + } +} diff --git a/src/LibHac/Fs/ValueStringBuilder.cs b/src/LibHac/FsSystem/ValueStringBuilder.cs similarity index 99% rename from src/LibHac/Fs/ValueStringBuilder.cs rename to src/LibHac/FsSystem/ValueStringBuilder.cs index 93a1f7da..4765aa4b 100644 --- a/src/LibHac/Fs/ValueStringBuilder.cs +++ b/src/LibHac/FsSystem/ValueStringBuilder.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace LibHac.Fs +namespace LibHac.FsSystem { internal ref struct ValueStringBuilder { diff --git a/src/LibHac/Horizon.cs b/src/LibHac/Horizon.cs index 6e55025d..302acee5 100644 --- a/src/LibHac/Horizon.cs +++ b/src/LibHac/Horizon.cs @@ -1,4 +1,6 @@ using LibHac.Fs; +using LibHac.FsService; +using LibHac.FsService.Creators; namespace LibHac { @@ -6,18 +8,32 @@ namespace LibHac { internal ITimeSpanGenerator Time { get; } - public FileSystemManager Fs { get; } + public FileSystemClient Fs { get; } + public FileSystemServer FsSrv { get; private set; } - public Horizon() - { - Fs = new FileSystemManager(this); - } + private readonly object _initLocker = new object(); public Horizon(ITimeSpanGenerator timer) { Time = timer; - Fs = new FileSystemManager(this, timer); + Fs = new FileSystemClient(timer); + } + + public void InitializeFileSystemServer(FileSystemCreators fsCreators, IDeviceOperator deviceOperator) + { + if (FsSrv != null) return; + + lock (_initLocker) + { + if (FsSrv != null) return; + + var config = new FileSystemServerConfig(); + config.FsCreators = fsCreators; + config.DeviceOperator = deviceOperator; + + FsSrv = new FileSystemServer(config); + } } } } diff --git a/src/LibHac/Keyset.cs b/src/LibHac/Keyset.cs index 61b5140a..5fe2064e 100644 --- a/src/LibHac/Keyset.cs +++ b/src/LibHac/Keyset.cs @@ -5,6 +5,9 @@ using System.Linq; using System.Security.Cryptography; using System.Text; using LibHac.Fs; +using LibHac.FsService; +using LibHac.FsSystem; +using LibHac.Spl; namespace LibHac { @@ -69,31 +72,48 @@ namespace LibHac public RSAParameters EticketExtKeyRsa { get; set; } - public bool KeysetForDev = false; - public byte[] NcaHdrFixedKeyModulus { - get { - if (KeysetForDev) { - return Keyset.NcaHdrFixedKeyModulusDev; - } else { - return Keyset.NcaHdrFixedKeyModulusProd; + public bool KeysetForDev; + public byte[] NcaHdrFixedKeyModulus + { + get + { + if (KeysetForDev) + { + return NcaHdrFixedKeyModulusDev; + } + else + { + return NcaHdrFixedKeyModulusProd; } } } - public byte[] AcidFixedKeyModulus { - get { - if (KeysetForDev) { - return Keyset.AcidFixedKeyModulusDev; - } else { - return Keyset.AcidFixedKeyModulusProd; + + public byte[] AcidFixedKeyModulus + { + get + { + if (KeysetForDev) + { + return AcidFixedKeyModulusDev; + } + else + { + return AcidFixedKeyModulusProd; } } } - public byte[] Package2FixedKeyModulus { - get { - if (KeysetForDev) { - return Keyset.Package2FixedKeyModulusDev; - } else { - return Keyset.Package2FixedKeyModulusProd; + + public byte[] Package2FixedKeyModulus + { + get + { + if (KeysetForDev) + { + return Package2FixedKeyModulusDev; + } + else + { + return Package2FixedKeyModulusProd; } } } @@ -218,7 +238,7 @@ namespace LibHac 0x71, 0xB0, 0x5C, 0xF4, 0xAD, 0x63, 0x4F, 0xC5, 0xE2, 0xAC, 0x1E, 0xC4, 0x33, 0x96, 0x09, 0x7B }; - public Dictionary TitleKeys { get; } = new Dictionary(new ByteArray128BitComparer()); + public ExternalKeySet ExternalKeySet { get; } = new ExternalKeySet(); public void SetSdSeed(byte[] sdseed) { @@ -287,7 +307,7 @@ namespace LibHac using (var keyblobDec = new Aes128CtrStorage( new MemoryStorage(EncryptedKeyblobs[i], 0x20, Keyblobs[i].Length), KeyblobKeys[i], counter, false)) { - keyblobDec.Read(Keyblobs[i], 0); + keyblobDec.Read(0, Keyblobs[i]).ThrowIfFailure(); } } } @@ -468,7 +488,7 @@ namespace LibHac } } - public static class ExternalKeys + public static class ExternalKeyReader { private const int TitleKeySize = 0x10; @@ -476,7 +496,7 @@ namespace LibHac public static readonly Dictionary UniqueKeyDict; public static readonly Dictionary AllKeyDict; - static ExternalKeys() + static ExternalKeyReader() { List commonKeys = CreateCommonKeyList(); List uniqueKeys = CreateUniqueKeyList(); @@ -491,6 +511,7 @@ namespace LibHac if (filename != null) ReadMainKeys(keyset, filename, AllKeyDict, logger); if (consoleKeysFilename != null) ReadMainKeys(keyset, consoleKeysFilename, AllKeyDict, logger); if (titleKeysFilename != null) ReadTitleKeys(keyset, titleKeysFilename, logger); + keyset.ExternalKeySet.TrimExcess(); keyset.DeriveKeys(logger); } @@ -596,7 +617,7 @@ namespace LibHac continue; } - keyset.TitleKeys[rightsId] = titleKey; + keyset.ExternalKeySet.Add(new RightsId(rightsId), new AccessKey(titleKey)).ThrowIfFailure(); } } } @@ -646,9 +667,9 @@ namespace LibHac { var sb = new StringBuilder(); - foreach (KeyValuePair kv in keyset.TitleKeys.OrderBy(x => x.Key.ToHexString())) + foreach ((RightsId rightsId, AccessKey key) kv in keyset.ExternalKeySet.ToList().OrderBy(x => x.rightsId.ToString())) { - string line = $"{kv.Key.ToHexString()} = {kv.Value.ToHexString()}"; + string line = $"{kv.rightsId} = {kv.key}"; sb.AppendLine(line); } diff --git a/src/LibHac/Kip.cs b/src/LibHac/Kip.cs index 0c154694..839bc544 100644 --- a/src/LibHac/Kip.cs +++ b/src/LibHac/Kip.cs @@ -1,6 +1,7 @@ using System; using System.IO; using LibHac.Fs; +using LibHac.FsSystem; namespace LibHac { @@ -43,9 +44,11 @@ namespace LibHac public byte[] DecompressSection(int index) { - IStorage compStream = OpenSection(index); - var compressed = new byte[compStream.GetSize()]; - compStream.Read(compressed, 0); + IStorage compStorage = OpenSection(index); + compStorage.GetSize(out long compressedSize).ThrowIfFailure(); + + var compressed = new byte[compressedSize]; + compStorage.Read(0, compressed).ThrowIfFailure(); return DecompressBlz(compressed); } diff --git a/src/LibHac/Kvdb/ImkvdbReader.cs b/src/LibHac/Kvdb/ImkvdbReader.cs index 28556809..e0d96df1 100644 --- a/src/LibHac/Kvdb/ImkvdbReader.cs +++ b/src/LibHac/Kvdb/ImkvdbReader.cs @@ -58,8 +58,8 @@ namespace LibHac.Kvdb key = default; value = default; - Result sizeResult = GetEntrySize(out int keySize, out int valueSize); - if (sizeResult.IsFailure()) return sizeResult; + Result rc = GetEntrySize(out int keySize, out int valueSize); + if (rc.IsFailure()) return rc; _position += Unsafe.SizeOf(); diff --git a/src/LibHac/Kvdb/KeyValueDatabase.cs b/src/LibHac/Kvdb/KeyValueDatabase.cs index f92d00fe..e2043074 100644 --- a/src/LibHac/Kvdb/KeyValueDatabase.cs +++ b/src/LibHac/Kvdb/KeyValueDatabase.cs @@ -37,13 +37,13 @@ namespace LibHac.Kvdb { var reader = new ImkvdbReader(data); - Result headerResult = reader.ReadHeader(out int entryCount); - if (headerResult.IsFailure()) return headerResult; + Result rc = reader.ReadHeader(out int entryCount); + if (rc.IsFailure()) return rc; for (int i = 0; i < entryCount; i++) { - Result entryResult = reader.ReadEntry(out ReadOnlySpan keyBytes, out ReadOnlySpan valueBytes); - if (entryResult.IsFailure()) return entryResult; + rc = reader.ReadEntry(out ReadOnlySpan keyBytes, out ReadOnlySpan valueBytes); + if (rc.IsFailure()) return rc; var key = new TKey(); var value = new TValue(); diff --git a/src/LibHac/LibHac.csproj b/src/LibHac/LibHac.csproj index 7091ca5c..c0e7f166 100644 --- a/src/LibHac/LibHac.csproj +++ b/src/LibHac/LibHac.csproj @@ -3,7 +3,7 @@ Library netcoreapp2.1;netstandard2.0;net46 - 7.3 + 8.0 @@ -23,6 +23,7 @@ true $(NoWarn);1591;NU5105 true + true diff --git a/src/LibHac/Ncm/ContentEnums.cs b/src/LibHac/Ncm/ContentEnums.cs new file mode 100644 index 00000000..e9197e82 --- /dev/null +++ b/src/LibHac/Ncm/ContentEnums.cs @@ -0,0 +1,40 @@ +namespace LibHac.Ncm +{ + public enum ContentType : byte + { + Meta = 0, + Program = 1, + Data = 2, + Control = 3, + HtmlDocument = 4, + LegalInformation = 5, + DeltaFragment = 6 + } + + public enum ContentMetaType : byte + { + SystemProgram = 1, + SystemData = 2, + SystemUpdate = 3, + BootImagePackage = 4, + BootImagePackageSafe = 5, + Application = 0x80, + Patch = 0x81, + AddOnContent = 0x82, + Delta = 0x83 + } + + public enum ContentMetaAttribute : byte + { + None = 0, + IncludesExFatDriver = 1, + Rebootless = 2 + } + + public enum UpdateType : byte + { + ApplyAsDelta = 0, + Overwrite = 1, + Create = 2 + } +} diff --git a/src/LibHac/Ncm/ContentId.cs b/src/LibHac/Ncm/ContentId.cs new file mode 100644 index 00000000..7df8ac93 --- /dev/null +++ b/src/LibHac/Ncm/ContentId.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Ncm +{ + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + public struct ContentId : IEquatable, IComparable, IComparable + { + public readonly Id128 Id; + + public ContentId(ulong high, ulong low) + { + Id = new Id128(high, low); + } + + public ContentId(ReadOnlySpan uid) + { + Id = new Id128(uid); + } + + public override string ToString() => Id.ToString(); + + public bool Equals(ContentId other) => Id == other.Id; + public override bool Equals(object obj) => obj is ContentId other && Equals(other); + + public override int GetHashCode() => Id.GetHashCode(); + + public int CompareTo(ContentId other) => Id.CompareTo(other.Id); + + public int CompareTo(object obj) + { + if (obj is null) return 1; + return obj is ContentId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(ContentId)}"); + } + + public void ToBytes(Span output) => Id.ToBytes(output); + + public ReadOnlySpan AsBytes() + { + return SpanHelpers.AsByteSpan(ref this); + } + + public static bool operator ==(ContentId left, ContentId right) => left.Equals(right); + public static bool operator !=(ContentId left, ContentId right) => !left.Equals(right); + + public static bool operator <(ContentId left, ContentId right) => left.CompareTo(right) < 0; + public static bool operator >(ContentId left, ContentId right) => left.CompareTo(right) > 0; + public static bool operator <=(ContentId left, ContentId right) => left.CompareTo(right) <= 0; + public static bool operator >=(ContentId left, ContentId right) => left.CompareTo(right) >= 0; + } +} \ No newline at end of file diff --git a/src/LibHac/Ncm/ContentMetaKey.cs b/src/LibHac/Ncm/ContentMetaKey.cs index fda438a4..bdbcbce2 100644 --- a/src/LibHac/Ncm/ContentMetaKey.cs +++ b/src/LibHac/Ncm/ContentMetaKey.cs @@ -8,8 +8,8 @@ namespace LibHac.Ncm { public ulong TitleId { get; private set; } public uint Version { get; private set; } - public byte Type { get; private set; } - public byte Flags { get; private set; } + public ContentMetaType Type { get; private set; } + public ContentMetaAttribute Attributes { get; private set; } public int ExportSize => 0x10; private bool _isFrozen; @@ -20,8 +20,8 @@ namespace LibHac.Ncm BinaryPrimitives.WriteUInt64LittleEndian(output, TitleId); BinaryPrimitives.WriteUInt32LittleEndian(output.Slice(8), Version); - output[0xC] = Type; - output[0xD] = Flags; + output[0xC] = (byte)Type; + output[0xD] = (byte)Attributes; } public void FromBytes(ReadOnlySpan input) @@ -31,8 +31,8 @@ namespace LibHac.Ncm TitleId = BinaryPrimitives.ReadUInt64LittleEndian(input); Version = BinaryPrimitives.ReadUInt32LittleEndian(input.Slice(8)); - Type = input[0xC]; - Flags = input[0xD]; + Type = (ContentMetaType)input[0xC]; + Attributes = (ContentMetaAttribute)input[0xD]; } public void Freeze() => _isFrozen = true; @@ -40,7 +40,7 @@ namespace LibHac.Ncm public bool Equals(ContentMetaKey other) { return other != null && TitleId == other.TitleId && Version == other.Version && - Type == other.Type && Flags == other.Flags; + Type == other.Type && Attributes == other.Attributes; } public override bool Equals(object obj) @@ -56,7 +56,7 @@ namespace LibHac.Ncm int hashCode = TitleId.GetHashCode(); hashCode = (hashCode * 397) ^ (int)Version; hashCode = (hashCode * 397) ^ Type.GetHashCode(); - hashCode = (hashCode * 397) ^ Flags.GetHashCode(); + hashCode = (hashCode * 397) ^ Attributes.GetHashCode(); return hashCode; // ReSharper restore NonReadonlyMemberInGetHashCode } @@ -72,7 +72,7 @@ namespace LibHac.Ncm if (versionComparison != 0) return versionComparison; int typeComparison = Type.CompareTo(other.Type); if (typeComparison != 0) return typeComparison; - return Flags.CompareTo(other.Flags); + return Attributes.CompareTo(other.Attributes); } public int CompareTo(object obj) diff --git a/src/LibHac/Ncm/ContentMetaStructs.cs b/src/LibHac/Ncm/ContentMetaStructs.cs new file mode 100644 index 00000000..992b115d --- /dev/null +++ b/src/LibHac/Ncm/ContentMetaStructs.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; + +namespace LibHac.Ncm +{ + [StructLayout(LayoutKind.Sequential, Size = 0x18, Pack = 1)] + public struct ContentInfo + { + public ContentId contentId; + public uint size1; + public ushort size2; + private ContentType contentType; + private byte IdOffset; + } + + public class ApplicationContentMetaKey + { + public ContentMetaKey Key { get; set; } + public ulong TitleId { get; set; } + } +} diff --git a/src/LibHac/Ncm/IContentMetaDatabase.cs b/src/LibHac/Ncm/IContentMetaDatabase.cs new file mode 100644 index 00000000..90307aee --- /dev/null +++ b/src/LibHac/Ncm/IContentMetaDatabase.cs @@ -0,0 +1,32 @@ +using System; + +namespace LibHac.Ncm +{ + public interface IContentMetaDatabase + { + Result Set(ContentMetaKey key, ReadOnlySpan value); + Result Get(out long valueSize, ContentMetaKey key, Span valueBuffer); + Result Remove(ContentMetaKey key); + Result GetContentIdByType(out ContentId contentId, ContentMetaKey key, ContentType type); + Result ListContentInfo(out int count, Span outInfo, ContentMetaKey key, int startIndex); + + Result List(out int totalEntryCount, out int matchedEntryCount, Span keys, ContentMetaType type, + ulong applicationTitleId, ulong minTitleId, ulong maxTitleId, ContentMetaAttribute attributes); + + Result GetLatestContentMetaKey(out ContentMetaKey key, ulong titleId); + Result ListApplication(out int totalEntryCount, out int matchedEntryCount, Span keys, ContentMetaType type); + Result Has(out bool hasKey, ContentMetaKey key); + Result HasAll(out bool hasAllKeys, ReadOnlySpan key); + Result GetSize(out long size, ContentMetaKey key); + Result GetRequiredSystemVersion(out int version, ContentMetaKey key); + Result GetPatchId(out ulong titleId, ContentMetaKey key); + Result DisableForcibly(); + Result LookupOrphanContent(Span outOrphaned, ReadOnlySpan contentIds); + Result Commit(); + Result HasContent(out bool hasContent, ContentMetaKey key, ContentId contentId); + Result ListContentMetaInfo(out int entryCount, Span outInfo, ContentMetaKey key, int startIndex); + Result GetAttributes(out ContentMetaAttribute attributes, ContentMetaKey key); + Result GetRequiredApplicationVersion(out int version, ContentMetaKey key); + //Result GetContentIdByTypeAndIdOffset(out ContentId contentId, ContentMetaKey key, ContentType type, byte idOffset); + } +} \ No newline at end of file diff --git a/src/LibHac/Ncm/IContentStorage.cs b/src/LibHac/Ncm/IContentStorage.cs new file mode 100644 index 00000000..2cd17ea9 --- /dev/null +++ b/src/LibHac/Ncm/IContentStorage.cs @@ -0,0 +1,37 @@ +using System; +using LibHac.Fs; + +namespace LibHac.Ncm +{ + public interface IContentStorage + { + Result GeneratePlaceHolderId(out PlaceHolderId placeHolderId); + Result CreatePlaceHolder(PlaceHolderId placeHolderId, ContentId contentId, long fileSize); + Result DeletePlaceHolder(PlaceHolderId placeHolderId); + Result HasPlaceHolder(out bool hasPlaceHolder, PlaceHolderId placeHolderId); + Result WritePlaceHolder(PlaceHolderId placeHolderId, long offset, ReadOnlySpan buffer); + Result Register(PlaceHolderId placeHolderId, ContentId contentId); + Result Delete(ContentId contentId); + Result Has(out bool hasContent, ContentId contentId); + Result GetPath(Span outPath, ContentId contentId); + Result GetPlaceHolderPath(Span outPath, PlaceHolderId placeHolderId); + Result CleanupAllPlaceHolder(); + Result ListPlaceHolder(out int count, Span placeHolderIds); + Result GetContentCount(out int count); + Result ListContentId(out int count, Span contentIds, int startOffset); + Result GetSizeFromContentId(out long size, ContentId contentId); + Result DisableForcibly(); + Result RevertToPlaceHolder(PlaceHolderId placeHolderId, ContentId oldContentId, ContentId newContentId); + Result SetPlaceHolderSize(PlaceHolderId placeHolderId, long size); + Result ReadContentIdFile(Span buffer, long size, ContentId contentId, long offset); + Result GetRightsIdFromPlaceHolderId(out RightsId rightsId, out byte keyGeneration, PlaceHolderId placeHolderId); + Result GetRightsIdFromContentId(out RightsId rightsId, out byte keyGeneration, ContentId contentId); + Result WriteContentForDebug(ContentId contentId, long offset, ReadOnlySpan buffer); + Result GetFreeSpaceSize(out long size); + Result GetTotalSpaceSize(out long size); + Result FlushPlaceHolder(); + //Result GetSizeFromPlaceHolderId(out long size, PlaceHolderId placeHolderId); + //Result RepairInvalidFileAttribute(); + //Result GetRightsIdFromPlaceHolderIdWithCache(out RightsId rightsId, out byte keyGeneration, PlaceHolderId placeHolderId, out ContentId cacheContentId); + } +} \ No newline at end of file diff --git a/src/LibHac/Ncm/PlaceHolderId.cs b/src/LibHac/Ncm/PlaceHolderId.cs new file mode 100644 index 00000000..27ed1631 --- /dev/null +++ b/src/LibHac/Ncm/PlaceHolderId.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Ncm +{ + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + public struct PlaceHolderId : IEquatable, IComparable, IComparable + { + public readonly Id128 Id; + + public PlaceHolderId(ulong high, ulong low) + { + Id = new Id128(high, low); + } + + public PlaceHolderId(ReadOnlySpan uid) + { + Id = new Id128(uid); + } + + public override string ToString() => Id.ToString(); + + public bool Equals(PlaceHolderId other) => Id == other.Id; + public override bool Equals(object obj) => obj is PlaceHolderId other && Equals(other); + + public override int GetHashCode() => Id.GetHashCode(); + + public int CompareTo(PlaceHolderId other) => Id.CompareTo(other.Id); + + public int CompareTo(object obj) + { + if (obj is null) return 1; + return obj is PlaceHolderId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(PlaceHolderId)}"); + } + + public void ToBytes(Span output) => Id.ToBytes(output); + + public ReadOnlySpan AsBytes() + { + return SpanHelpers.AsByteSpan(ref this); + } + + public static bool operator ==(PlaceHolderId left, PlaceHolderId right) => left.Equals(right); + public static bool operator !=(PlaceHolderId left, PlaceHolderId right) => !left.Equals(right); + + public static bool operator <(PlaceHolderId left, PlaceHolderId right) => left.CompareTo(right) < 0; + public static bool operator >(PlaceHolderId left, PlaceHolderId right) => left.CompareTo(right) > 0; + public static bool operator <=(PlaceHolderId left, PlaceHolderId right) => left.CompareTo(right) <= 0; + public static bool operator >=(PlaceHolderId left, PlaceHolderId right) => left.CompareTo(right) >= 0; + } +} \ No newline at end of file diff --git a/src/LibHac/Ncm/StorageId.cs b/src/LibHac/Ncm/StorageId.cs new file mode 100644 index 00000000..1fd4339a --- /dev/null +++ b/src/LibHac/Ncm/StorageId.cs @@ -0,0 +1,12 @@ +namespace LibHac.Ncm +{ + public enum StorageId : byte + { + None = 0, + Host = 1, + GameCard = 2, + BuiltInSystem = 3, + BuiltInUser = 4, + SdCard = 5 + } +} diff --git a/src/LibHac/Ncm/TitleId.cs b/src/LibHac/Ncm/TitleId.cs new file mode 100644 index 00000000..0288bd7b --- /dev/null +++ b/src/LibHac/Ncm/TitleId.cs @@ -0,0 +1,17 @@ +using System.Diagnostics; + +namespace LibHac.Ncm +{ + [DebuggerDisplay("{" + nameof(Value) + "}")] + public struct TitleId + { + public readonly ulong Value; + + public TitleId(ulong value) + { + Value = value; + } + + public static explicit operator ulong(TitleId titleId) => titleId.Value; + } +} diff --git a/src/LibHac/Nro.cs b/src/LibHac/Nro.cs index 2e09ea80..d5cca492 100644 --- a/src/LibHac/Nro.cs +++ b/src/LibHac/Nro.cs @@ -1,5 +1,6 @@ using System.IO; using LibHac.Fs; +using LibHac.FsSystem; namespace LibHac { @@ -21,7 +22,9 @@ namespace LibHac if (Header.Magic != "NRO0") throw new InvalidDataException("NRO0 magic is incorrect!"); - if (Header.Size < Storage.GetSize()) + Storage.GetSize(out long storageSize).ThrowIfFailure(); + + if (Header.Size < storageSize) { AssetStorage = Storage.Slice(Header.Size); var assetReader = new BinaryReader(AssetStorage.AsStream()); diff --git a/src/LibHac/Nso.cs b/src/LibHac/Nso.cs index d60a76ca..59dfa245 100644 --- a/src/LibHac/Nso.cs +++ b/src/LibHac/Nso.cs @@ -1,6 +1,7 @@ using System.Collections; using System.IO; using LibHac.Fs; +using LibHac.FsSystem; namespace LibHac { @@ -82,7 +83,7 @@ namespace LibHac public byte[] DecompressSection() { var compressed = new byte[CompressedSize]; - OpenSection().Read(compressed, 0); + OpenSection().Read(0, compressed).ThrowIfFailure(); if (IsCompressed) return Lz4.Decompress(compressed, (int)DecompressedSize); diff --git a/src/LibHac/Package1.cs b/src/LibHac/Package1.cs index 6f57cf98..f8dc9947 100644 --- a/src/LibHac/Package1.cs +++ b/src/LibHac/Package1.cs @@ -1,6 +1,7 @@ using System; using System.IO; using LibHac.Fs; +using LibHac.FsSystem; namespace LibHac { @@ -40,7 +41,7 @@ namespace LibHac for (int i = 0; i < 0x20; i++) { var dec = new Aes128CtrStorage(encStorage, keyset.Package1Keys[i], Counter, true); - dec.Read(decBuffer, 0); + dec.Read(0, decBuffer).ThrowIfFailure(); if (BitConverter.ToUInt32(decBuffer, 0) == Pk11Magic) { diff --git a/src/LibHac/Package2.cs b/src/LibHac/Package2.cs index 9714bc25..9e9cf4c6 100644 --- a/src/LibHac/Package2.cs +++ b/src/LibHac/Package2.cs @@ -1,6 +1,7 @@ using System; using System.IO; using LibHac.Fs; +using LibHac.FsSystem; namespace LibHac { @@ -109,12 +110,12 @@ namespace LibHac var counter = new byte[0x10]; var decBuffer = new byte[0x10]; - storage.Read(counter, 0x100); + storage.Read(0x100, counter).ThrowIfFailure(); for (int i = 0; i < 0x20; i++) { var dec = new Aes128CtrStorage(storage.Slice(0x100), keyset.Package2Keys[i], counter, false); - dec.Read(decBuffer, 0x50); + dec.Read(0x50, decBuffer).ThrowIfFailure(); if (BitConverter.ToUInt32(decBuffer, 0) == Pk21Magic) { diff --git a/src/LibHac/Result.cs b/src/LibHac/Result.cs index 86fcb228..33c2875d 100644 --- a/src/LibHac/Result.cs +++ b/src/LibHac/Result.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; namespace LibHac { [Serializable] + [DebuggerDisplay("{ToString()}")] public struct Result : IEquatable { public readonly int Value; @@ -32,6 +34,22 @@ namespace LibHac } } + /// + /// A function that can contain code for logging or debugging returned results. + /// Intended to be used when returning a non-zero Result: + /// return result.Log(); + /// + /// The called value. + public Result Log() + { + return this; + } + + public override string ToString() + { + return IsSuccess() ? "Success" : ErrorCode; + } + public override bool Equals(object obj) { return obj is Result result && Equals(result); diff --git a/src/LibHac/Spl/AccessKey.cs b/src/LibHac/Spl/AccessKey.cs new file mode 100644 index 00000000..2a8e0be2 --- /dev/null +++ b/src/LibHac/Spl/AccessKey.cs @@ -0,0 +1,29 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibHac.Common; + +namespace LibHac.Spl +{ + [DebuggerDisplay("{ToString()}")] + [StructLayout(LayoutKind.Sequential, Size = 0x10)] + public struct AccessKey : IEquatable + { + private readonly Key128 Key; + + public ReadOnlySpan Value => SpanHelpers.AsByteSpan(ref this); + + public AccessKey(ReadOnlySpan bytes) + { + Key = new Key128(bytes); + } + + public override string ToString() => Key.ToString(); + + public override bool Equals(object obj) => obj is AccessKey key && Equals(key); + public bool Equals(AccessKey other) => Key.Equals(other.Key); + public override int GetHashCode() => Key.GetHashCode(); + public static bool operator ==(AccessKey left, AccessKey right) => left.Equals(right); + public static bool operator !=(AccessKey left, AccessKey right) => !(left == right); + } +} diff --git a/src/hactoolnet/TimeSpanTimer.cs b/src/LibHac/StopWatchTimeSpanGenerator.cs similarity index 70% rename from src/hactoolnet/TimeSpanTimer.cs rename to src/LibHac/StopWatchTimeSpanGenerator.cs index 9235181d..c10e9da2 100644 --- a/src/hactoolnet/TimeSpanTimer.cs +++ b/src/LibHac/StopWatchTimeSpanGenerator.cs @@ -1,10 +1,9 @@ using System; using System.Diagnostics; -using LibHac; -namespace hactoolnet +namespace LibHac { - public class TimeSpanTimer : ITimeSpanGenerator + public class StopWatchTimeSpanGenerator : ITimeSpanGenerator { private Stopwatch Timer = Stopwatch.StartNew(); diff --git a/src/LibHac/SwitchFs.cs b/src/LibHac/SwitchFs.cs index b3070ec7..f39f369c 100644 --- a/src/LibHac/SwitchFs.cs +++ b/src/LibHac/SwitchFs.cs @@ -4,8 +4,10 @@ using System.Diagnostics; using System.IO; using System.Linq; using LibHac.Fs; -using LibHac.Fs.NcaUtils; -using LibHac.Fs.Save; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; +using LibHac.FsSystem.Save; +using LibHac.Ncm; namespace LibHac { @@ -67,21 +69,20 @@ namespace LibHac private void OpenAllNcas() { // Todo: give warning if directories named "*.nca" are found or manually fix the archive bit - IEnumerable files = ContentFs.OpenDirectory("/", OpenDirectoryMode.All) - .EnumerateEntries("*.nca", SearchOptions.RecurseSubdirectories) + IEnumerable files = ContentFs.EnumerateEntries("*.nca", SearchOptions.RecurseSubdirectories) .Where(x => x.Type == DirectoryEntryType.File); - foreach (DirectoryEntry fileEntry in files) + foreach (DirectoryEntryEx fileEntry in files) { SwitchFsNca nca = null; try { - IStorage storage = ContentFs.OpenFile(fileEntry.FullPath, OpenMode.Read).AsStorage(); + ContentFs.OpenFile(out IFile ncaFile, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure(); - nca = new SwitchFsNca(new Nca(Keyset, storage)); + nca = new SwitchFsNca(new Nca(Keyset, ncaFile.AsStorage())); nca.NcaId = GetNcaFilename(fileEntry.Name, nca); - string extension = nca.Nca.Header.ContentType == ContentType.Meta ? ".cnmt.nca" : ".nca"; + string extension = nca.Nca.Header.ContentType == NcaContentType.Meta ? ".cnmt.nca" : ".nca"; nca.Filename = nca.NcaId + extension; } catch (MissingKeyException ex) @@ -107,14 +108,15 @@ namespace LibHac { if (SaveFs == null) return; - foreach (DirectoryEntry fileEntry in SaveFs.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File)) + foreach (DirectoryEntryEx fileEntry in SaveFs.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File)) { SaveDataFileSystem save = null; string saveName = Path.GetFileNameWithoutExtension(fileEntry.Name); try { - IFile file = SaveFs.OpenFile(fileEntry.FullPath, OpenMode.Read); + SaveFs.OpenFile(out IFile file, fileEntry.FullPath, OpenMode.Read).ThrowIfFailure(); + save = new SaveDataFileSystem(Keyset, file.AsStorage(), IntegrityCheckLevel.None, true); } catch (Exception ex) @@ -131,16 +133,16 @@ namespace LibHac private void ReadTitles() { - foreach (SwitchFsNca nca in Ncas.Values.Where(x => x.Nca.Header.ContentType == ContentType.Meta)) + foreach (SwitchFsNca nca in Ncas.Values.Where(x => x.Nca.Header.ContentType == NcaContentType.Meta)) { try { var title = new Title(); IFileSystem fs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid); - string cnmtPath = fs.EnumerateEntries("*.cnmt").Single().FullPath; + string cnmtPath = fs.EnumerateEntries("/", "*.cnmt").Single().FullPath; - IFile file = fs.OpenFile(cnmtPath, OpenMode.Read); + fs.OpenFile(out IFile file, cnmtPath, OpenMode.Read).ThrowIfFailure(); var metadata = new Cnmt(file.AsStream()); title.Id = metadata.TitleId; @@ -160,11 +162,11 @@ namespace LibHac switch (content.Type) { - case CnmtContentType.Program: - case CnmtContentType.Data: + case ContentType.Program: + case ContentType.Data: title.MainNca = contentNca; break; - case CnmtContentType.Control: + case ContentType.Control: title.ControlNca = contentNca; break; } @@ -184,7 +186,7 @@ namespace LibHac foreach (Title title in Titles.Values.Where(x => x.ControlNca != null)) { IFileSystem romfs = title.ControlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid); - IFile control = romfs.OpenFile("control.nacp", OpenMode.Read); + romfs.OpenFile(out IFile control, "control.nacp", OpenMode.Read).ThrowIfFailure(); title.Control = new Nacp(control.AsStream()); @@ -201,7 +203,7 @@ namespace LibHac private void CreateApplications() { - foreach (Title title in Titles.Values.Where(x => x.Metadata.Type >= TitleType.Application)) + foreach (Title title in Titles.Values.Where(x => x.Metadata.Type >= ContentMetaType.Application)) { Cnmt meta = title.Metadata; ulong appId = meta.ApplicationTitleId; @@ -229,7 +231,7 @@ namespace LibHac private string GetNcaFilename(string name, SwitchFsNca nca) { - if (nca.Nca.Header.ContentType != ContentType.Meta || !name.EndsWith(".cnmt.nca")) + if (nca.Nca.Header.ContentType != NcaContentType.Meta || !name.EndsWith(".cnmt.nca")) { return Path.GetFileNameWithoutExtension(name); } @@ -343,16 +345,16 @@ namespace LibHac switch (title.Metadata.Type) { - case TitleType.Application: + case ContentMetaType.Application: Main = title; break; - case TitleType.Patch: + case ContentMetaType.Patch: Patch = title; break; - case TitleType.AddOnContent: + case ContentMetaType.AddOnContent: AddOnContent.Add(title); break; - case TitleType.Delta: + case ContentMetaType.Delta: break; } diff --git a/src/LibHac/Util.cs b/src/LibHac/Util.cs index d9f1a5e0..ea20c212 100644 --- a/src/LibHac/Util.cs +++ b/src/LibHac/Util.cs @@ -354,7 +354,9 @@ namespace LibHac public static string ToHexString(this byte[] bytes) => ToHexString(bytes.AsSpan()); - public static string ToHexString(this Span bytes) + public static string ToHexString(this Span bytes) => ToHexString((ReadOnlySpan)bytes); + + public static string ToHexString(this ReadOnlySpan bytes) { uint[] lookup32 = Lookup32; var result = new char[bytes.Length * 2]; diff --git a/src/LibHac/Xci.cs b/src/LibHac/Xci.cs index 3587f12f..b8250571 100644 --- a/src/LibHac/Xci.cs +++ b/src/LibHac/Xci.cs @@ -1,4 +1,5 @@ using LibHac.Fs; +using LibHac.FsSystem; namespace LibHac { @@ -28,8 +29,8 @@ namespace LibHac XciPartition root = GetRootPartition(); if (type == XciPartitionType.Root) return root; - IStorage partitionStorage = root.OpenFile(type.GetFileName(), OpenMode.Read).AsStorage(); - return new XciPartition(partitionStorage); + root.OpenFile(out IFile partitionFile, type.GetFileName(), OpenMode.Read).ThrowIfFailure(); + return new XciPartition(partitionFile.AsStorage()); } private XciPartition GetRootPartition() diff --git a/src/LibHac/XciHeader.cs b/src/LibHac/XciHeader.cs index f945c8a3..361dcad8 100644 --- a/src/LibHac/XciHeader.cs +++ b/src/LibHac/XciHeader.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text; +using LibHac.Fs; namespace LibHac { @@ -36,9 +37,9 @@ namespace LibHac public int BackupAreaStartPage { get; set; } public byte KekIndex { get; set; } public byte TitleKeyDecIndex { get; set; } - public RomSize RomSize { get; set; } + public GameCardSize GameCardSize { get; set; } public byte CardHeaderVersion { get; set; } - public XciFlags Flags { get; set; } + public GameCardAttribute Flags { get; set; } public ulong PackageId { get; set; } public long ValidDataEndPage { get; set; } public byte[] AesCbcIv { get; set; } @@ -62,8 +63,9 @@ namespace LibHac public byte[] UppHash { get; set; } public ulong UppId { get; set; } - public Validity SignatureValidity { get; set; } + public byte[] ImageHash { get; } + public Validity SignatureValidity { get; set; } public Validity PartitionFsHeaderValidity { get; set; } public XciHeader(Keyset keyset, Stream stream) @@ -88,9 +90,9 @@ namespace LibHac byte keyIndex = reader.ReadByte(); KekIndex = (byte)(keyIndex >> 4); TitleKeyDecIndex = (byte)(keyIndex & 7); - RomSize = (RomSize)reader.ReadByte(); + GameCardSize = (GameCardSize)reader.ReadByte(); CardHeaderVersion = reader.ReadByte(); - Flags = (XciFlags)reader.ReadByte(); + Flags = (GameCardAttribute)reader.ReadByte(); PackageId = reader.ReadUInt64(); ValidDataEndPage = reader.ReadInt64(); AesCbcIv = reader.ReadBytes(Crypto.Aes128Size); @@ -104,7 +106,7 @@ namespace LibHac SelKey = reader.ReadInt32(); LimAreaPage = reader.ReadInt32(); - if (!keyset.XciHeaderKey.IsEmpty()) + if (keyset != null && !keyset.XciHeaderKey.IsEmpty()) { byte[] encHeader = reader.ReadBytes(EncryptedHeaderSize); var decHeader = new byte[EncryptedHeaderSize]; @@ -126,30 +128,14 @@ namespace LibHac } } + ImageHash = Crypto.ComputeSha256(sigData, 0, sigData.Length); + reader.BaseStream.Position = RootPartitionOffset; PartitionFsHeaderValidity = Crypto.CheckMemoryHashTable(reader.ReadBytes((int)RootPartitionHeaderSize), RootPartitionHeaderHash, 0, (int)RootPartitionHeaderSize); } } } - public enum RomSize - { - Size1Gb = 0xFA, - Size2Gb = 0xF8, - Size4Gb = 0xF0, - Size8Gb = 0xE0, - Size16Gb = 0xE1, - Size32Gb = 0xE2 - } - - [Flags] - public enum XciFlags - { - AutoBoot = 1 << 0, - HistoryErase = 1 << 1, - RepairTool = 1 << 2 - } - public enum CardClockRate { ClockRate25 = 10551312, diff --git a/src/hactoolnet/AccessLog.cs b/src/hactoolnet/AccessLog.cs index 89226520..67ba128f 100644 --- a/src/hactoolnet/AccessLog.cs +++ b/src/hactoolnet/AccessLog.cs @@ -2,15 +2,15 @@ using System.IO; using System.Runtime.CompilerServices; using LibHac; -using LibHac.Fs.Accessors; +using LibHac.Fs; namespace hactoolnet { public class ConsoleAccessLog : IAccessLog { - public void Log(TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = "") + public void Log(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = "") { - Console.WriteLine(CommonAccessLog.BuildLogLine(startTime, endTime, handleId, message, caller)); + Console.WriteLine(AccessLogHelpers.BuildDefaultLogLine(result, startTime, endTime, handleId, message, caller)); } } @@ -22,9 +22,9 @@ namespace hactoolnet Logger = logger; } - public void Log(TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = "") + public void Log(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = "") { - Logger.LogMessage(CommonAccessLog.BuildLogLine(startTime, endTime, handleId, message, caller)); + Logger.LogMessage(AccessLogHelpers.BuildDefaultLogLine(result, startTime, endTime, handleId, message, caller)); } } @@ -37,18 +37,9 @@ namespace hactoolnet Logger = logger; } - public void Log(TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = "") + public void Log(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, string message, [CallerMemberName] string caller = "") { - Logger.WriteLine(CommonAccessLog.BuildLogLine(startTime, endTime, handleId, message, caller)); - } - } - - public static class CommonAccessLog - { - public static string BuildLogLine(TimeSpan startTime, TimeSpan endTime, int handleId, string message, - string caller) - { - return $"FS_ACCESS: {{ start: {(long)startTime.TotalMilliseconds,9}, end: {(long)endTime.TotalMilliseconds,9}, handle: 0x{handleId:x8}, function: \"{caller}\"{message} }}"; + Logger.WriteLine(AccessLogHelpers.BuildDefaultLogLine(result, startTime, endTime, handleId, message, caller)); } } } diff --git a/src/hactoolnet/FsUtils.cs b/src/hactoolnet/FsUtils.cs index 6ece0c28..e7d06748 100644 --- a/src/hactoolnet/FsUtils.cs +++ b/src/hactoolnet/FsUtils.cs @@ -2,13 +2,13 @@ using System.Buffers; using LibHac; using LibHac.Fs; -using LibHac.Fs.Accessors; +using LibHac.FsSystem; namespace hactoolnet { public static class FsUtils { - public static void CopyDirectoryWithProgress(FileSystemManager fs, string sourcePath, string destPath, + public static void CopyDirectoryWithProgress(FileSystemClient fs, string sourcePath, string destPath, CreateFileOptions options = CreateFileOptions.None, IProgressReport logger = null) { try @@ -23,12 +23,14 @@ namespace hactoolnet } } - private static void CopyDirectoryWithProgressInternal(FileSystemManager fs, string sourcePath, string destPath, + private static void CopyDirectoryWithProgressInternal(FileSystemClient fs, string sourcePath, string destPath, CreateFileOptions options, IProgressReport logger) { - using (DirectoryHandle sourceHandle = fs.OpenDirectory(sourcePath, OpenDirectoryMode.All)) + fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath, OpenDirectoryMode.All).ThrowIfFailure(); + + using (sourceHandle) { - foreach (DirectoryEntry entry in fs.ReadDirectory(sourceHandle)) + foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default)) { string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name)); string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name)); @@ -51,11 +53,11 @@ namespace hactoolnet } } - public static long GetTotalSize(FileSystemManager fs, string path, string searchPattern = "*") + public static long GetTotalSize(FileSystemClient fs, string path, string searchPattern = "*") { long size = 0; - foreach (DirectoryEntry entry in fs.EnumerateEntries(path, searchPattern)) + foreach (DirectoryEntryEx entry in fs.EnumerateEntries(path, searchPattern)) { size += entry.Size; } @@ -63,37 +65,53 @@ namespace hactoolnet return size; } - public static void CopyFileWithProgress(FileSystemManager fs, string sourcePath, string destPath, IProgressReport logger = null) + public static Result CopyFileWithProgress(FileSystemClient fs, string sourcePath, string destPath, IProgressReport logger = null) { - using (FileHandle sourceHandle = fs.OpenFile(sourcePath, OpenMode.Read)) - using (FileHandle destHandle = fs.OpenFile(destPath, OpenMode.Write | OpenMode.Append)) + Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath, OpenMode.Read); + if (rc.IsFailure()) return rc; + + using (sourceHandle) { - const int maxBufferSize = 1024 * 1024; + rc = fs.OpenFile(out FileHandle destHandle, destPath, OpenMode.Write | OpenMode.AllowAppend); + if (rc.IsFailure()) return rc; - long fileSize = fs.GetFileSize(sourceHandle); - int bufferSize = (int)Math.Min(maxBufferSize, fileSize); - - byte[] buffer = ArrayPool.Shared.Rent(bufferSize); - try + using (destHandle) { - for (long offset = 0; offset < fileSize; offset += bufferSize) + const int maxBufferSize = 1024 * 1024; + + rc = fs.GetFileSize(out long fileSize, sourceHandle); + if (rc.IsFailure()) return rc; + + int bufferSize = (int)Math.Min(maxBufferSize, fileSize); + + byte[] buffer = ArrayPool.Shared.Rent(bufferSize); + try { - int toRead = (int)Math.Min(fileSize - offset, bufferSize); - Span buf = buffer.AsSpan(0, toRead); + for (long offset = 0; offset < fileSize; offset += bufferSize) + { + int toRead = (int)Math.Min(fileSize - offset, bufferSize); + Span buf = buffer.AsSpan(0, toRead); - fs.ReadFile(sourceHandle, buf, offset); - fs.WriteFile(destHandle, buf, offset); + rc = fs.ReadFile(out long _, sourceHandle, offset, buf); + if (rc.IsFailure()) return rc; - logger?.ReportAdd(toRead); + rc = fs.WriteFile(destHandle, offset, buf); + if (rc.IsFailure()) return rc; + + logger?.ReportAdd(toRead); + } + } + finally + { + ArrayPool.Shared.Return(buffer); } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - fs.FlushFile(destHandle); + rc = fs.FlushFile(destHandle); + if (rc.IsFailure()) return rc; + } } + + return Result.Success; } } } diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs index 1a327eaf..c99ac0a8 100644 --- a/src/hactoolnet/Options.cs +++ b/src/hactoolnet/Options.cs @@ -1,5 +1,5 @@ using LibHac; -using LibHac.Fs; +using LibHac.FsSystem; namespace hactoolnet { diff --git a/src/hactoolnet/Print.cs b/src/hactoolnet/Print.cs index 0cea2972..ab5b6348 100644 --- a/src/hactoolnet/Print.cs +++ b/src/hactoolnet/Print.cs @@ -2,8 +2,8 @@ using System.Buffers.Binary; using System.Text; using LibHac; -using LibHac.Fs; -using LibHac.Fs.NcaUtils; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; namespace hactoolnet { diff --git a/src/hactoolnet/ProcessBench.cs b/src/hactoolnet/ProcessBench.cs index b68776a2..e464c39d 100644 --- a/src/hactoolnet/ProcessBench.cs +++ b/src/hactoolnet/ProcessBench.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using LibHac; using LibHac.Fs; +using LibHac.FsSystem; namespace hactoolnet { @@ -25,7 +26,9 @@ namespace hactoolnet encryptWatch.Stop(); logger.SetTotal(0); - string rate = Util.GetBytesReadable((long)(src.GetSize() * iterations / encryptWatch.Elapsed.TotalSeconds)); + src.GetSize(out long srcSize).ThrowIfFailure(); + + string rate = Util.GetBytesReadable((long)(srcSize * iterations / encryptWatch.Elapsed.TotalSeconds)); logger.LogMessage($"{label}{rate}/s"); } diff --git a/src/hactoolnet/ProcessDelta.cs b/src/hactoolnet/ProcessDelta.cs index 0c966a34..3f80126b 100644 --- a/src/hactoolnet/ProcessDelta.cs +++ b/src/hactoolnet/ProcessDelta.cs @@ -3,7 +3,8 @@ using System.IO; using System.Runtime.InteropServices; using System.Text; using LibHac.Fs; -using LibHac.Fs.NcaUtils; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; using static hactoolnet.Print; namespace hactoolnet @@ -19,7 +20,7 @@ namespace hactoolnet { IStorage deltaStorage = deltaFile; Span magic = stackalloc byte[4]; - deltaFile.Read(magic, 0); + deltaFile.Read(0, magic).ThrowIfFailure(); if (MemoryMarshal.Read(magic) != Ndv0Magic) { @@ -33,12 +34,14 @@ namespace hactoolnet throw new FileNotFoundException("Specified NCA does not contain a delta fragment"); } - deltaStorage = fs.OpenFile(FragmentFileName, OpenMode.Read).AsStorage(); + fs.OpenFile(out IFile deltaFragmentFile, FragmentFileName, OpenMode.Read).ThrowIfFailure(); + + deltaStorage = deltaFragmentFile.AsStorage(); } catch (InvalidDataException) { } // Ignore non-NCA3 files } - var delta = new DeltaFragment(deltaStorage); + var delta = new Delta(deltaStorage); if (ctx.Options.BaseFile != null) { @@ -51,7 +54,9 @@ namespace hactoolnet using (var outFile = new FileStream(ctx.Options.OutFile, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { IStorage patchedStorage = delta.GetPatchedStorage(); - patchedStorage.CopyToStream(outFile, patchedStorage.GetSize(), ctx.Logger); + patchedStorage.GetSize(out long patchedStorageSize).ThrowIfFailure(); + + patchedStorage.CopyToStream(outFile, patchedStorageSize, ctx.Logger); } } } @@ -61,18 +66,18 @@ namespace hactoolnet } } - private static string Print(this DeltaFragment delta) + private static string Print(this Delta delta) { int colLen = 36; var sb = new StringBuilder(); sb.AppendLine(); - sb.AppendLine("Delta Fragment:"); + sb.AppendLine("Delta File:"); PrintItem(sb, colLen, "Magic:", delta.Header.Magic); PrintItem(sb, colLen, "Base file size:", $"0x{delta.Header.OriginalSize:x12}"); PrintItem(sb, colLen, "New file size:", $"0x{delta.Header.NewSize:x12}"); - PrintItem(sb, colLen, "Fragment header size:", $"0x{delta.Header.FragmentHeaderSize:x12}"); - PrintItem(sb, colLen, "Fragment body size:", $"0x{delta.Header.FragmentBodySize:x12}"); + PrintItem(sb, colLen, "Delta header size:", $"0x{delta.Header.HeaderSize:x12}"); + PrintItem(sb, colLen, "Delta body size:", $"0x{delta.Header.BodySize:x12}"); return sb.ToString(); } diff --git a/src/hactoolnet/ProcessFsBuild.cs b/src/hactoolnet/ProcessFsBuild.cs index 5d0d06ab..3f56759a 100644 --- a/src/hactoolnet/ProcessFsBuild.cs +++ b/src/hactoolnet/ProcessFsBuild.cs @@ -1,6 +1,7 @@ using System.IO; using LibHac.Fs; -using LibHac.Fs.RomFs; +using LibHac.FsSystem; +using LibHac.FsSystem.RomFs; namespace hactoolnet { @@ -17,13 +18,15 @@ namespace hactoolnet var localFs = new LocalFileSystem(ctx.Options.InFile); var builder = new RomFsBuilder(localFs); - IStorage romfs = builder.Build(); + IStorage romFs = builder.Build(); ctx.Logger.LogMessage($"Building RomFS as {ctx.Options.OutFile}"); + romFs.GetSize(out long romFsSize).ThrowIfFailure(); + using (var outFile = new FileStream(ctx.Options.OutFile, FileMode.Create, FileAccess.ReadWrite)) { - romfs.CopyToStream(outFile, romfs.GetSize(), ctx.Logger); + romFs.CopyToStream(outFile, romFsSize, ctx.Logger); } ctx.Logger.LogMessage($"Finished writing {ctx.Options.OutFile}"); @@ -48,9 +51,11 @@ namespace hactoolnet ctx.Logger.LogMessage($"Building Partition FS as {ctx.Options.OutFile}"); + partitionFs.GetSize(out long partitionFsSize).ThrowIfFailure(); + using (var outFile = new FileStream(ctx.Options.OutFile, FileMode.Create, FileAccess.ReadWrite)) { - partitionFs.CopyToStream(outFile, partitionFs.GetSize(), ctx.Logger); + partitionFs.CopyToStream(outFile, partitionFsSize, ctx.Logger); } ctx.Logger.LogMessage($"Finished writing {ctx.Options.OutFile}"); diff --git a/src/hactoolnet/ProcessKip.cs b/src/hactoolnet/ProcessKip.cs index b10e4d35..1cbfacbc 100644 --- a/src/hactoolnet/ProcessKip.cs +++ b/src/hactoolnet/ProcessKip.cs @@ -1,6 +1,6 @@ using System.IO; using LibHac; -using LibHac.Fs; +using LibHac.FsSystem; namespace hactoolnet { diff --git a/src/hactoolnet/ProcessNca.cs b/src/hactoolnet/ProcessNca.cs index 590619fc..f6125e29 100644 --- a/src/hactoolnet/ProcessNca.cs +++ b/src/hactoolnet/ProcessNca.cs @@ -1,8 +1,10 @@ using System.IO; using System.Text; using LibHac; +using LibHac.Common; using LibHac.Fs; -using LibHac.Fs.NcaUtils; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; using LibHac.Npdm; using static hactoolnet.Print; @@ -42,12 +44,12 @@ namespace hactoolnet if (ctx.Options.SectionOutDir[i] != null) { - FileSystemManager fs = ctx.Horizon.Fs; + FileSystemClient fs = ctx.Horizon.Fs; string mountName = $"section{i}"; - fs.Register(mountName, OpenFileSystem(i)); - fs.Register("output", new LocalFileSystem(ctx.Options.SectionOutDir[i])); + fs.Register(mountName.ToU8Span(), OpenFileSystem(i)); + fs.Register("output".ToU8Span(), new LocalFileSystem(ctx.Options.SectionOutDir[i])); FsUtils.CopyDirectoryWithProgress(fs, mountName + ":/", "output:/", logger: ctx.Logger); @@ -72,7 +74,7 @@ namespace hactoolnet { IFileSystem romfs = OpenFileSystemByType(NcaSectionType.Data); - foreach (DirectoryEntry entry in romfs.EnumerateEntries()) + foreach (DirectoryEntryEx entry in romfs.EnumerateEntries()) { ctx.Logger.LogMessage(entry.FullPath); } @@ -93,10 +95,10 @@ namespace hactoolnet if (ctx.Options.RomfsOutDir != null) { - FileSystemManager fs = ctx.Horizon.Fs; + FileSystemClient fs = ctx.Horizon.Fs; - fs.Register("rom", OpenFileSystemByType(NcaSectionType.Data)); - fs.Register("output", new LocalFileSystem(ctx.Options.RomfsOutDir)); + fs.Register("rom".ToU8Span(), OpenFileSystemByType(NcaSectionType.Data)); + fs.Register("output".ToU8Span(), new LocalFileSystem(ctx.Options.RomfsOutDir)); FsUtils.CopyDirectoryWithProgress(fs, "rom:/", "output:/", logger: ctx.Logger); @@ -108,9 +110,12 @@ namespace hactoolnet { long bytesToRead = 1024L * 1024 * 1024 * 5; IStorage storage = OpenStorageByType(NcaSectionType.Data); - var dest = new NullStorage(storage.GetSize()); - int iterations = (int)(bytesToRead / storage.GetSize()) + 1; + storage.GetSize(out long sectionSize).ThrowIfFailure(); + + var dest = new NullStorage(sectionSize); + + int iterations = (int)(bytesToRead / sectionSize) + 1; ctx.Logger.LogMessage(iterations.ToString()); ctx.Logger.StartNewStopWatch(); @@ -128,7 +133,7 @@ namespace hactoolnet if (ctx.Options.ExefsOutDir != null || ctx.Options.ExefsOut != null) { - if (nca.Header.ContentType != ContentType.Program) + if (nca.Header.ContentType != NcaContentType.Program) { ctx.Logger.LogMessage("NCA's content type is not \"Program\""); return; @@ -147,10 +152,10 @@ namespace hactoolnet if (ctx.Options.ExefsOutDir != null) { - FileSystemManager fs = ctx.Horizon.Fs; + FileSystemClient fs = ctx.Horizon.Fs; - fs.Register("code", OpenFileSystemByType(NcaSectionType.Code)); - fs.Register("output", new LocalFileSystem(ctx.Options.ExefsOutDir)); + fs.Register("code".ToU8Span(), OpenFileSystemByType(NcaSectionType.Code)); + fs.Register("output".ToU8Span(), new LocalFileSystem(ctx.Options.ExefsOutDir)); FsUtils.CopyDirectoryWithProgress(fs, "code:/", "output:/", logger: ctx.Logger); @@ -201,13 +206,13 @@ namespace hactoolnet private static Validity VerifySignature2(this Nca nca) { - if (nca.Header.ContentType != ContentType.Program) return Validity.Unchecked; + if (nca.Header.ContentType != NcaContentType.Program) return Validity.Unchecked; IFileSystem pfs = nca.OpenFileSystem(NcaSectionType.Code, IntegrityCheckLevel.ErrorOnInvalid); if (!pfs.FileExists("main.npdm")) return Validity.Unchecked; - IFile npdmStorage = pfs.OpenFile("main.npdm", OpenMode.Read); - var npdm = new NpdmBinary(npdmStorage.AsStream()); + pfs.OpenFile(out IFile npdmFile, "main.npdm", OpenMode.Read).ThrowIfFailure(); + var npdm = new NpdmBinary(npdmFile.AsStream()); return nca.Header.VerifySignature2(npdm.AciD.Rsa2048Modulus); } @@ -266,7 +271,7 @@ namespace hactoolnet if (!nca.Header.IsSectionEnabled(i)) continue; NcaFsHeader sectHeader = nca.Header.GetFsHeader(i); - bool isExefs = nca.Header.ContentType == ContentType.Program && i == 0; + bool isExefs = nca.Header.ContentType == NcaContentType.Program && i == 0; sb.AppendLine($" Section {i}:"); PrintItem(sb, colLen, " Offset:", $"0x{nca.Header.GetSectionStartOffset(i):x12}"); diff --git a/src/hactoolnet/ProcessPackage.cs b/src/hactoolnet/ProcessPackage.cs index ee551196..7d2c043d 100644 --- a/src/hactoolnet/ProcessPackage.cs +++ b/src/hactoolnet/ProcessPackage.cs @@ -1,7 +1,7 @@ using System.IO; using System.Text; using LibHac; -using LibHac.Fs; +using LibHac.FsSystem; using static hactoolnet.Print; namespace hactoolnet diff --git a/src/hactoolnet/ProcessPfs.cs b/src/hactoolnet/ProcessPfs.cs index d36c2fec..fd7bbf2b 100644 --- a/src/hactoolnet/ProcessPfs.cs +++ b/src/hactoolnet/ProcessPfs.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Text; using LibHac; using LibHac.Fs; +using LibHac.FsSystem; using static hactoolnet.Print; namespace hactoolnet @@ -89,11 +90,13 @@ namespace hactoolnet Assembly thisAssembly = Assembly.GetExecutingAssembly(); Stream cert = thisAssembly.GetManifestResourceStream("hactoolnet.CA00000003_XS00000020"); builder.AddFile($"{ticket.RightsId.ToHexString()}.cert", cert.AsIFile(OpenMode.Read)); - + using (var outStream = new FileStream(ctx.Options.NspOut, FileMode.Create, FileAccess.ReadWrite)) { IStorage builtPfs = builder.Build(PartitionFileSystemType.Standard); - builtPfs.CopyToStream(outStream, builtPfs.GetSize(), ctx.Logger); + builtPfs.GetSize(out long pfsSize).ThrowIfFailure(); + + builtPfs.CopyToStream(outStream, pfsSize, ctx.Logger); } } } diff --git a/src/hactoolnet/ProcessRomfs.cs b/src/hactoolnet/ProcessRomfs.cs index cf53df5a..093816ee 100644 --- a/src/hactoolnet/ProcessRomfs.cs +++ b/src/hactoolnet/ProcessRomfs.cs @@ -1,6 +1,7 @@ using System.IO; using LibHac.Fs; -using LibHac.Fs.RomFs; +using LibHac.FsSystem; +using LibHac.FsSystem.RomFs; namespace hactoolnet { @@ -20,7 +21,7 @@ namespace hactoolnet if (ctx.Options.ListRomFs) { - foreach (DirectoryEntry entry in romfs.EnumerateEntries()) + foreach (DirectoryEntryEx entry in romfs.EnumerateEntries()) { ctx.Logger.LogMessage(entry.FullPath); } @@ -28,9 +29,11 @@ namespace hactoolnet if (ctx.Options.RomfsOut != null) { + romfsStorage.GetSize(out long romFsSize).ThrowIfFailure(); + using (var outFile = new FileStream(ctx.Options.RomfsOut, FileMode.Create, FileAccess.ReadWrite)) { - romfsStorage.CopyToStream(outFile, romfsStorage.GetSize(), ctx.Logger); + romfsStorage.CopyToStream(outFile, romFsSize, ctx.Logger); } } diff --git a/src/hactoolnet/ProcessSave.cs b/src/hactoolnet/ProcessSave.cs index 349a6dc7..1a18db38 100644 --- a/src/hactoolnet/ProcessSave.cs +++ b/src/hactoolnet/ProcessSave.cs @@ -4,8 +4,10 @@ using System.IO; using System.Linq; using System.Text; using LibHac; +using LibHac.Common; using LibHac.Fs; -using LibHac.Fs.Save; +using LibHac.FsSystem; +using LibHac.FsSystem.Save; using static hactoolnet.Print; namespace hactoolnet @@ -27,9 +29,9 @@ namespace hactoolnet bool signNeeded = ctx.Options.SignSave; var save = new SaveDataFileSystem(ctx.Keyset, file, ctx.Options.IntegrityLevel, true); - FileSystemManager fs = ctx.Horizon.Fs; + FileSystemClient fs = ctx.Horizon.Fs; - fs.Register("save", save); + fs.Register("save".ToU8Span(), save); if (ctx.Options.Validate) { @@ -38,7 +40,7 @@ namespace hactoolnet if (ctx.Options.OutDir != null) { - fs.Register("output", new LocalFileSystem(ctx.Options.OutDir)); + fs.Register("output".ToU8Span(), new LocalFileSystem(ctx.Options.OutDir)); FsUtils.CopyDirectoryWithProgress(fs, "save:/", "output:/", logger: ctx.Logger); @@ -61,11 +63,16 @@ namespace hactoolnet using (IFile inFile = new LocalFile(ctx.Options.ReplaceFileSource, OpenMode.Read)) { - using (IFile outFile = save.OpenFile(destFilename, OpenMode.ReadWrite)) + save.OpenFile(out IFile outFile, destFilename, OpenMode.ReadWrite).ThrowIfFailure(); + + using (outFile) { - if (inFile.GetSize() != outFile.GetSize()) + inFile.GetSize(out long inFileSize).ThrowIfFailure(); + outFile.GetSize(out long outFileSize).ThrowIfFailure(); + + if (inFileSize != outFileSize) { - outFile.SetSize(inFile.GetSize()); + outFile.SetSize(inFileSize).ThrowIfFailure(); } inFile.CopyTo(outFile, ctx.Logger); @@ -79,7 +86,7 @@ namespace hactoolnet if (ctx.Options.RepackSource != null) { - fs.Register("input", new LocalFileSystem(ctx.Options.RepackSource)); + fs.Register("input".ToU8Span(), new LocalFileSystem(ctx.Options.RepackSource)); fs.CleanDirectoryRecursively("save:/"); fs.Commit("save"); @@ -96,7 +103,7 @@ namespace hactoolnet { if (signNeeded) { - save.Commit(ctx.Keyset); + save.Commit(ctx.Keyset).ThrowIfFailure(); signNeeded = false; } } @@ -110,7 +117,7 @@ namespace hactoolnet if (signNeeded) { - if (save.Commit(ctx.Keyset)) + if (save.Commit(ctx.Keyset).IsSuccess()) { ctx.Logger.LogMessage("Successfully signed save file"); } @@ -125,7 +132,7 @@ namespace hactoolnet if (ctx.Options.ListFiles) { - foreach (DirectoryEntry entry in save.EnumerateEntries()) + foreach (DirectoryEntryEx entry in save.EnumerateEntries()) { ctx.Logger.LogMessage(entry.FullPath); } @@ -222,7 +229,7 @@ namespace hactoolnet { var sb = new StringBuilder(); - foreach (DirectoryEntry entry in save.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File)) + foreach (DirectoryEntryEx entry in save.EnumerateEntries().Where(x => x.Type == DirectoryEntryType.File)) { save.FileTable.TryOpenFile(entry.FullPath, out SaveFileInfo fileInfo); if (fileInfo.StartBlock < 0) continue; @@ -306,7 +313,7 @@ namespace hactoolnet var sb = new StringBuilder(); sb.AppendLine(); - long freeSpace = save.GetFreeSpaceSize(""); + save.GetFreeSpaceSize(out long freeSpace, "").ThrowIfFailure(); sb.AppendLine("Savefile:"); PrintItem(sb, colLen, $"CMAC Signature{save.Header.SignatureValidity.GetValidityString()}:", save.Header.Cmac); diff --git a/src/hactoolnet/ProcessSwitchFs.cs b/src/hactoolnet/ProcessSwitchFs.cs index 35ead159..a2fa567d 100644 --- a/src/hactoolnet/ProcessSwitchFs.cs +++ b/src/hactoolnet/ProcessSwitchFs.cs @@ -5,8 +5,9 @@ using System.Linq; using System.Text; using LibHac; using LibHac.Fs; -using LibHac.Fs.NcaUtils; -using LibHac.Fs.Save; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; +using LibHac.FsSystem.Save; #if NETCOREAPP using System.Runtime.InteropServices; @@ -316,7 +317,7 @@ namespace hactoolnet IFileSystem fs = switchFs.ContentFs; - DirectoryEntry[] ncaDirs = fs.EnumerateEntries("*.nca", SearchOptions.RecurseSubdirectories) + DirectoryEntryEx[] ncaDirs = fs.EnumerateEntries("*.nca", SearchOptions.RecurseSubdirectories) .Where(x => x.Type == DirectoryEntryType.Directory) .Where(x => fs.FileExists($"{x.FullPath}/00")) .ToArray(); @@ -326,7 +327,7 @@ namespace hactoolnet ctx.Logger.LogMessage("Warning: NCA folders without the archive flag were found. Fixing..."); } - foreach (DirectoryEntry file in ncaDirs) + foreach (DirectoryEntryEx file in ncaDirs) { fs.SetConcatenationFileAttribute(file.FullPath); ctx.Logger.LogMessage($"{file.FullPath}"); diff --git a/src/hactoolnet/ProcessXci.cs b/src/hactoolnet/ProcessXci.cs index cbddcfdb..43c36f48 100644 --- a/src/hactoolnet/ProcessXci.cs +++ b/src/hactoolnet/ProcessXci.cs @@ -4,7 +4,8 @@ using System.Linq; using System.Text; using LibHac; using LibHac.Fs; -using LibHac.Fs.NcaUtils; +using LibHac.FsSystem; +using LibHac.FsSystem.NcaUtils; using static hactoolnet.Print; namespace hactoolnet @@ -127,7 +128,7 @@ namespace hactoolnet IStorage ncaStorage = partition.OpenFile(fileEntry, OpenMode.Read).AsStorage(); var nca = new Nca(ctx.Keyset, ncaStorage); - if (nca.Header.ContentType == ContentType.Program) + if (nca.Header.ContentType == NcaContentType.Program) { mainNca = nca; } @@ -148,7 +149,7 @@ namespace hactoolnet PrintItem(sb, colLen, "Magic:", xci.Header.Magic); PrintItem(sb, colLen, $"Header Signature{xci.Header.SignatureValidity.GetValidityString()}:", xci.Header.Signature); PrintItem(sb, colLen, $"Header Hash{xci.Header.PartitionFsHeaderValidity.GetValidityString()}:", xci.Header.RootPartitionHeaderHash); - PrintItem(sb, colLen, "Cartridge Type:", GetCartridgeType(xci.Header.RomSize)); + PrintItem(sb, colLen, "Cartridge Type:", GetCartridgeType(xci.Header.GameCardSize)); PrintItem(sb, colLen, "Cartridge Size:", $"0x{Util.MediaToReal(xci.Header.ValidDataEndPage + 1):x12}"); PrintItem(sb, colLen, "Header IV:", xci.Header.AesCbcIv); @@ -191,16 +192,16 @@ namespace hactoolnet } } - private static string GetCartridgeType(RomSize size) + private static string GetCartridgeType(GameCardSize size) { switch (size) { - case RomSize.Size1Gb: return "1GB"; - case RomSize.Size2Gb: return "2GB"; - case RomSize.Size4Gb: return "4GB"; - case RomSize.Size8Gb: return "8GB"; - case RomSize.Size16Gb: return "16GB"; - case RomSize.Size32Gb: return "32GB"; + case GameCardSize.Size1Gb: return "1GB"; + case GameCardSize.Size2Gb: return "2GB"; + case GameCardSize.Size4Gb: return "4GB"; + case GameCardSize.Size8Gb: return "8GB"; + case GameCardSize.Size16Gb: return "16GB"; + case GameCardSize.Size32Gb: return "32GB"; default: return string.Empty; } } diff --git a/src/hactoolnet/Program.cs b/src/hactoolnet/Program.cs index 112b7db1..fdd01016 100644 --- a/src/hactoolnet/Program.cs +++ b/src/hactoolnet/Program.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text; using LibHac; +using LibHac.Fs; namespace hactoolnet { @@ -57,13 +58,17 @@ namespace hactoolnet using (var logger = new ProgressBar()) { ctx.Logger = logger; - ctx.Horizon = new Horizon(new TimeSpanTimer()); + ctx.Horizon = new Horizon(new StopWatchTimeSpanGenerator()); if (ctx.Options.AccessLog != null) { logWriter = new StreamWriter(ctx.Options.AccessLog); var accessLog = new TextWriterAccessLog(logWriter); - ctx.Horizon.Fs.SetAccessLog(true, accessLog); + + ctx.Horizon.Fs.SetLocalAccessLogMode(LocalAccessLogMode.All); + ctx.Horizon.Fs.SetGlobalAccessLogMode(GlobalAccessLogMode.Log); + + ctx.Horizon.Fs.SetAccessLogObject(accessLog); } OpenKeyset(ctx); @@ -169,7 +174,7 @@ namespace hactoolnet consoleKeyFile = homeConsoleKeyFile; } - ctx.Keyset = ExternalKeys.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile, ctx.Logger, ctx.Options.UseDevKeys); + ctx.Keyset = ExternalKeyReader.ReadKeyFile(keyFile, titleKeyFile, consoleKeyFile, ctx.Logger, ctx.Options.UseDevKeys); if (ctx.Options.SdSeed != null) { ctx.Keyset.SetSdSeed(ctx.Options.SdSeed.ToBytes()); @@ -180,15 +185,15 @@ namespace hactoolnet string dir = ctx.Options.OutDir; Directory.CreateDirectory(dir); - File.WriteAllText(Path.Combine(dir, keyFileName), ExternalKeys.PrintCommonKeys(ctx.Keyset)); - File.WriteAllText(Path.Combine(dir, "console.keys"), ExternalKeys.PrintUniqueKeys(ctx.Keyset)); - File.WriteAllText(Path.Combine(dir, "title.keys"), ExternalKeys.PrintTitleKeys(ctx.Keyset)); + File.WriteAllText(Path.Combine(dir, keyFileName), ExternalKeyReader.PrintCommonKeys(ctx.Keyset)); + File.WriteAllText(Path.Combine(dir, "console.keys"), ExternalKeyReader.PrintUniqueKeys(ctx.Keyset)); + File.WriteAllText(Path.Combine(dir, "title.keys"), ExternalKeyReader.PrintTitleKeys(ctx.Keyset)); } } private static void ProcessKeygen(Context ctx) { - Console.WriteLine(ExternalKeys.PrintCommonKeys(ctx.Keyset)); + Console.WriteLine(ExternalKeyReader.PrintCommonKeys(ctx.Keyset)); } // For running random stuff diff --git a/tests/LibHac.Tests/AesXts.cs b/tests/LibHac.Tests/AesXts.cs index b2dd51dd..df7a9629 100644 --- a/tests/LibHac.Tests/AesXts.cs +++ b/tests/LibHac.Tests/AesXts.cs @@ -1,5 +1,5 @@ using System.Linq; -using LibHac.Fs; +using LibHac.FsSystem; using Xunit; namespace LibHac.Tests diff --git a/tests/LibHac.Tests/PathToolsTests.cs b/tests/LibHac.Tests/PathToolsTests.cs index fb3a04d9..ff4330fe 100644 --- a/tests/LibHac.Tests/PathToolsTests.cs +++ b/tests/LibHac.Tests/PathToolsTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using LibHac.Fs; +using LibHac.FsSystem; using Xunit; namespace LibHac.Tests diff --git a/tests/LibHac.Tests/RomFsTests.cs b/tests/LibHac.Tests/RomFsTests.cs index bce481ad..5163e8fa 100644 --- a/tests/LibHac.Tests/RomFsTests.cs +++ b/tests/LibHac.Tests/RomFsTests.cs @@ -1,5 +1,5 @@ using System; -using LibHac.Fs.RomFs; +using LibHac.FsSystem.RomFs; using Xunit; namespace LibHac.Tests