Merge pull request #83 from Thealexbarney/fssrv

Add the base/beginnings of a FileSystem service
This commit is contained in:
Alex Barney 2019-10-12 20:18:15 -05:00 committed by GitHub
commit db88a46d07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
262 changed files with 11078 additions and 5089 deletions

View File

@ -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
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace LibHac.Common
{
public ref struct BlitStruct<T> where T : unmanaged
{
private readonly Span<T> _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<T> data)
{
_buffer = data;
Debug.Assert(_buffer.Length != 0);
}
public BlitStruct(Span<byte> data)
{
_buffer = MemoryMarshal.Cast<byte, T>(data);
Debug.Assert(_buffer.Length != 0);
}
public BlitStruct(ref T data)
{
_buffer = SpanHelpers.AsSpan(ref data);
}
public Span<byte> GetByteSpan()
{
return MemoryMarshal.Cast<T, byte>(_buffer);
}
public Span<byte> GetByteSpan(int elementIndex)
{
Span<T> element = _buffer.Slice(elementIndex, 1);
return MemoryMarshal.Cast<T, byte>(element);
}
public static int QueryByteLength(int elementCount)
{
return Unsafe.SizeOf<T>() * elementCount;
}
}
}

View File

@ -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
};
}
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace LibHac.Common
{
public interface ITimeStampGenerator
{
DateTimeOffset Generate();
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace LibHac.Common
{
/// <summary>
/// A generic 128-bit ID value.
/// </summary>
[DebuggerDisplay("{ToString()}")]
[StructLayout(LayoutKind.Sequential, Size = 0x10)]
public struct Id128 : IEquatable<Id128>, IComparable<Id128>, 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<byte> uid)
{
ReadOnlySpan<ulong> longs = MemoryMarshal.Cast<byte, ulong>(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<byte> output)
{
Span<ulong> longs = MemoryMarshal.Cast<byte, ulong>(output);
longs[0] = High;
longs[1] = Low;
}
public ReadOnlySpan<byte> 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;
}
}

View File

@ -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<Key128>
{
private readonly ulong _dummy1;
private readonly ulong _dummy2;
public Span<byte> Value => SpanHelpers.AsByteSpan(ref this);
public Key128(ReadOnlySpan<byte> bytes)
{
ReadOnlySpan<ulong> longs = MemoryMarshal.Cast<byte, ulong>(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);
}
}

View File

@ -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;
}
}

View File

@ -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<T> CreateSpan<T>(ref T reference, int length)
{
return MemoryMarshal.CreateSpan(ref reference, length);
}
#else
public static unsafe Span<T> CreateSpan<T>(ref T reference, int length)
{
return new Span<T>(Unsafe.AsPointer(ref reference), length);
}
#endif
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span<T> AsSpan<T>(ref T reference) where T : unmanaged
{
return CreateSpan(ref reference, 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span<byte> AsByteSpan<T>(ref T reference) where T : unmanaged
{
Span<T> span = AsSpan(ref reference);
return MemoryMarshal.Cast<T, byte>(span);
}
}
}

View File

@ -0,0 +1,95 @@
using System;
using System.Text;
namespace LibHac.Common
{
public static class StringUtils
{
public static int Copy(Span<byte> dest, ReadOnlySpan<byte> 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<byte> s)
{
int i = 0;
while (i < s.Length && s[i] != 0)
{
i++;
}
return i;
}
public static int GetLength(ReadOnlySpan<byte> s, int maxLen)
{
int i = 0;
while (i < maxLen && i < s.Length && s[i] != 0)
{
i++;
}
return i;
}
/// <summary>
/// Concatenates 2 byte strings.
/// </summary>
/// <param name="dest"></param>
/// <param name="source"></param>
/// <returns>The length of the resulting string.</returns>
/// <remarks>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.</remarks>
public static int Concat(Span<byte> dest, ReadOnlySpan<byte> source)
{
return Concat(dest, GetLength(dest), source);
}
public static int Concat(Span<byte> dest, int destLength, ReadOnlySpan<byte> 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<byte> value)
{
#if STRING_SPAN
return Encoding.UTF8.GetString(value);
#else
return Encoding.UTF8.GetString(value.ToArray());
#endif
}
public static string Utf8ZToString(ReadOnlySpan<byte> value)
{
return Utf8ToString(value.Slice(0, GetLength(value)));
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Diagnostics;
using System.Text;
namespace LibHac.Common
{
[DebuggerDisplay("{ToString()}")]
public ref struct U8Span
{
private readonly ReadOnlySpan<byte> _buffer;
public ReadOnlySpan<byte> Value => _buffer;
public int Length => _buffer.Length;
public byte this[int i]
{
get => _buffer[i];
}
public U8Span(ReadOnlySpan<byte> value)
{
_buffer = value;
}
public U8Span(string value)
{
_buffer = Encoding.UTF8.GetBytes(value);
}
public static implicit operator ReadOnlySpan<byte>(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;
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Diagnostics;
using System.Text;
namespace LibHac.Common
{
[DebuggerDisplay("{ToString()}")]
public ref struct U8SpanMutable
{
private readonly Span<byte> _buffer;
public Span<byte> Value => _buffer;
public int Length => _buffer.Length;
public byte this[int i]
{
get => _buffer[i];
set => _buffer[i] = value;
}
public U8SpanMutable(Span<byte> 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<byte>(U8SpanMutable value) => value.Value;
public static implicit operator Span<byte>(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;
}
}

View File

@ -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<byte> 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<byte>(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;
}
}

View File

@ -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<byte> _buffer;
private int _length;
public bool Overflowed { get; private set; }
public int Capacity => _buffer.Length - NullTerminatorLength;
public U8StringBuilder(Span<byte> buffer)
{
_buffer = buffer;
_length = 0;
Overflowed = false;
ThrowIfBufferLengthIsZero();
AddNullTerminator();
}
public U8StringBuilder Append(ReadOnlySpan<byte> 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);
}
}

View File

@ -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);
}
}
}

View File

@ -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<byte> 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<byte>(U8StringMutable value) => value.Value;
public static implicit operator Span<byte>(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;
}
}

View File

@ -2,7 +2,7 @@
using System.IO;
using System.Numerics;
using System.Security.Cryptography;
using LibHac.Fs;
using LibHac.FsSystem;
namespace LibHac
{

View File

@ -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} }}";
}
}
}

View File

@ -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<DirectoryEntry> Read()
public IEnumerable<DirectoryEntryEx> Read()
{
CheckIfDisposed();
return Directory.Read();
return ParentFs.EnumerateEntries(Path, "*", SearchOptions.Default);
}
public int GetEntryCount()
public Result Read(out long entriesRead, Span<DirectoryEntry> 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()

View File

@ -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<byte> destination, long offset, ReadOption options)
protected override Result ReadImpl(out long bytesRead, long offset, Span<byte> destination, ReadOption options)
{
CheckIfDisposed();
return File.Read(destination, offset, options);
return File.Read(out bytesRead, offset, destination, options);
}
public void Write(ReadOnlySpan<byte> source, long offset, WriteOption options)
protected override Result WriteImpl(long offset, ReadOnlySpan<byte> 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();
}

View File

@ -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<FileAccessor> OpenFiles { get; } = new HashSet<FileAccessor>();
private HashSet<DirectoryAccessor> OpenDirectories { get; } = new HashSet<DirectoryAccessor>();
@ -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<byte> outBuffer, ReadOnlySpan<byte> inBuffer, string path, QueryId queryId)
public Result QueryEntry(Span<byte> outBuffer, ReadOnlySpan<byte> inBuffer, string path, QueryId queryId)
{
FileSystem.QueryEntry(outBuffer, inBuffer, path, queryId);
return FileSystem.QueryEntry(outBuffer, inBuffer, queryId, path);
}
public Result GetCommonMountName(Span<byte> nameBuffer)
{
if (MountNameGenerator == null) return ResultFs.PreconditionViolation;
return MountNameGenerator.Generate(nameBuffer);
}
internal void NotifyCloseFile(FileAccessor file)

View File

@ -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 = "");
}
}

View File

@ -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<DirectoryEntry> 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();
}
/// <summary>
/// Reads the size of a NAX0 file from its header. Returns -1 on error.
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
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;
}
}
}
}

View File

@ -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]);
}
/// <summary>
/// Creates a new <see cref="AesXtsFile"/> using the provided key.
/// </summary>
/// <param name="path">The full path of the file to create.</param>
/// <param name="size">The initial size of the created file.</param>
/// <param name="options">Flags to control how the file is created.
/// Should usually be <see cref="CreateFileOptions.None"/></param>
/// <param name="key">The 256-bit key containing a 128-bit data key followed by a 128-bit tweak key.</param>
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<byte> outBuffer, ReadOnlySpan<byte> 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);
}
}
}
}

View File

@ -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<DirectoryEntry> 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
}
}
}

View File

@ -1,325 +0,0 @@
using System;
using System.Collections.Generic;
#if CROSS_PLATFORM
using System.Runtime.InteropServices;
#endif
namespace LibHac.Fs
{
/// <summary>
/// An <see cref="IFileSystem"/> that stores large files as smaller, separate sub-files.
/// </summary>
/// <remarks>
/// 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 <see cref="ConcatenationFileSystem"/> 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 <see cref="SubFileSize"/> that was specified
/// at the creation of the <see cref="ConcatenationFileSystem"/>.
/// </remarks>
public class ConcatenationFileSystem : IFileSystem
{
private const long DefaultSubFileSize = 0xFFFF0000; // Hard-coded value used by FS
private IAttributeFileSystem BaseFileSystem { get; }
private long SubFileSize { get; }
/// <summary>
/// Initializes a new <see cref="ConcatenationFileSystem"/>.
/// </summary>
/// <param name="baseFileSystem">The base <see cref="IAttributeFileSystem"/> for the
/// new <see cref="ConcatenationFileSystem"/>.</param>
public ConcatenationFileSystem(IAttributeFileSystem baseFileSystem) : this(baseFileSystem, DefaultSubFileSize) { }
/// <summary>
/// Initializes a new <see cref="ConcatenationFileSystem"/>.
/// </summary>
/// <param name="baseFileSystem">The base <see cref="IAttributeFileSystem"/> for the
/// new <see cref="ConcatenationFileSystem"/>.</param>
/// <param name="subFileSize">The size of each sub-file. Once a file exceeds this size, a new sub-file will be created</param>
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<IFile>();
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<byte> outBuffer, ReadOnlySpan<byte> 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;
}
}
}

View File

@ -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<byte> nameBuffer)
{
U8String mountName = GetContentStorageMountName(StorageId);
int length = StringUtils.Copy(nameBuffer, mountName);
nameBuffer[length] = (byte)':';
nameBuffer[length + 1] = 0;
return Result.Success;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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<byte> 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,

View File

@ -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);
}
}
}

View File

@ -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<byte> destination, long offset, ReadOption options)
{
return BaseFile.Read(destination, offset, options);
}
public override void Write(ReadOnlySpan<byte> 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();
}
}
}
}
}

View File

@ -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<byte> outBuffer, ReadOnlySpan<byte> 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--;
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<IDisposable> ToDispose { get; } = new List<IDisposable>();
// 0 = not disposed; 1 = disposed
private int _disposedState;
private bool IsDisposed => _disposedState != 0;
public abstract int Read(Span<byte> destination, long offset, ReadOption options);
public abstract void Write(ReadOnlySpan<byte> 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<byte> destination, ReadOption options);
protected abstract Result WriteImpl(long offset, ReadOnlySpan<byte> 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<byte> span, long offset)
protected virtual Result OperateRangeImpl(Span<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> 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<byte> span, long offset)
public Result Read(out long bytesRead, long offset, Span<byte> 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<byte> 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<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> 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;
}
}
/// <summary>
/// Specifies which operations are available on an <see cref="IFile"/>.
/// </summary>
[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
}
}

View File

@ -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);
}
}
}

View File

@ -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<byte> 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<byte> 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);
}
}
}
}

View File

@ -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<byte> destination, long offset)
{
BaseFile.Read(destination, offset);
}
protected override void WriteImpl(ReadOnlySpan<byte> 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);
}
}
}

View File

@ -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<byte> outBuffer, ReadOnlySpan<byte> 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<byte> outBuffer, ReadOnlySpan<byte> 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) { }
}
}

View File

@ -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<Result> operation,
Func<string> 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<Result> operation,
Func<string> 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
}
}

View File

@ -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<DirectoryEntry> 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();
}
}
}
}

View File

@ -0,0 +1,109 @@
using System;
namespace LibHac.Fs
{
public partial class FileSystemClient
{
public Result ReadFile(FileHandle handle, long offset, Span<byte> destination)
{
return ReadFile(handle, offset, destination, ReadOption.None);
}
public Result ReadFile(FileHandle handle, long offset, Span<byte> 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<byte> destination)
{
return ReadFile(out bytesRead, handle, offset, destination, ReadOption.None);
}
public Result ReadFile(out long bytesRead, FileHandle handle, long offset, Span<byte> 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<byte> source)
{
return WriteFile(handle, offset, source, WriteOption.None);
}
public Result WriteFile(FileHandle handle, long offset, ReadOnlySpan<byte> 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);
}
}
}

View File

@ -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<char> 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<char> 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<char> 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<char> 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<char> 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<char> 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<char> oldSubPath);
if (rc.IsFailure()) return rc;
rc = FindFileSystem(newPath.AsSpan(), out FileSystemAccessor newFileSystem, out ReadOnlySpan<char> 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<char> oldSubPath);
if (rc.IsFailure()) return rc;
rc = FindFileSystem(newPath.AsSpan(), out FileSystemAccessor newFileSystem, out ReadOnlySpan<char> 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<char> 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<char> 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<char> 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<char> 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<char> 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<char> 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;
}
}
}

View File

@ -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<char> path, out FileSystemAccessor fileSystem, out ReadOnlySpan<char> subPath)
{
fileSystem = default;
Result rc = GetMountName(path, out ReadOnlySpan<char> 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<char> path, out ReadOnlySpan<char> mountName, out ReadOnlySpan<char> 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;
}
}
}

View File

@ -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<byte>.Shared.Rent(bufferSize);
try
{
for (long offset = 0; offset < fileSize; offset += bufferSize)
{
int toRead = (int)Math.Min(fileSize - offset, bufferSize);
Span<byte> 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<byte>.Shared.Return(buffer);
logger?.SetTotal(0);
}
rc = fs.FlushFile(destHandle);
if (rc.IsFailure()) return rc;
}
}
return Result.Success;
}
public static IEnumerable<DirectoryEntryEx> EnumerateEntries(this FileSystemClient fs, string path)
{
return fs.EnumerateEntries(path, "*");
}
public static IEnumerable<DirectoryEntryEx> EnumerateEntries(this FileSystemClient fs, string path, string searchPattern)
{
return fs.EnumerateEntries(path, searchPattern, SearchOptions.RecurseSubdirectories);
}
public static IEnumerable<DirectoryEntryEx> 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<DirectoryEntryEx> 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;
}
}
}

View File

@ -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<char> 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<char> 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<char> 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<char> 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<char> 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<char> 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<char> oldSubPath)
.ThrowIfFailure();
FindFileSystem(newPath.AsSpan(), out FileSystemAccessor newFileSystem, out ReadOnlySpan<char> 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<char> oldSubPath)
.ThrowIfFailure();
FindFileSystem(newPath.AsSpan(), out FileSystemAccessor newFileSystem, out ReadOnlySpan<char> 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<char> 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<char> 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<char> 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<char> subPath)
.ThrowIfFailure();
return fileSystem.GetFreeSpaceSize(subPath.ToString());
}
public long GetTotalSpaceSize(string path)
{
FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan<char> subPath)
.ThrowIfFailure();
return fileSystem.GetTotalSpaceSize(subPath.ToString());
}
public FileTimeStampRaw GetFileTimeStamp(string path)
{
FindFileSystem(path.AsSpan(), out FileSystemAccessor fileSystem, out ReadOnlySpan<char> 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<byte> destination, long offset)
{
return ReadFile(handle, destination, offset, ReadOption.None);
}
public int ReadFile(FileHandle handle, Span<byte> 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<byte> source, long offset)
{
WriteFile(handle, source, offset, WriteOption.None);
}
public void WriteFile(FileHandle handle, ReadOnlySpan<byte> 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<DirectoryEntry> ReadDirectory(DirectoryHandle handle)
{
if (IsEnabledAccessLog() && IsEnabledHandleAccessLog(handle))
{
TimeSpan startTime = Time.GetCurrent();
IEnumerable<DirectoryEntry> 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<char> path, out FileSystemAccessor fileSystem, out ReadOnlySpan<char> subPath)
{
fileSystem = default;
Result result = GetMountName(path, out ReadOnlySpan<char> 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<char> path, out ReadOnlySpan<char> mountName, out ReadOnlySpan<char> 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);
}
}
}

View File

@ -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<byte>.Shared.Rent(bufferSize);
try
{
for (long offset = 0; offset < fileSize; offset += bufferSize)
{
int toRead = (int)Math.Min(fileSize - offset, bufferSize);
Span<byte> buf = buffer.AsSpan(0, toRead);
fs.ReadFile(sourceHandle, buf, offset);
fs.WriteFile(destHandle, buf, offset);
logger?.ReportAdd(toRead);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
logger?.SetTotal(0);
}
fs.FlushFile(destHandle);
}
}
public static IEnumerable<DirectoryEntry> EnumerateEntries(this FileSystemManager fs, string path)
{
return fs.EnumerateEntries(path, "*");
}
public static IEnumerable<DirectoryEntry> EnumerateEntries(this FileSystemManager fs, string path, string searchPattern)
{
return fs.EnumerateEntries(path, searchPattern, SearchOptions.RecurseSubdirectories);
}
public static IEnumerable<DirectoryEntry> 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<DirectoryEntry> 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);
}
}
}

130
src/LibHac/Fs/FsEnums.cs Normal file
View File

@ -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
}
/// <summary>
/// Specifies which operations are available on an <see cref="IFile"/>.
/// </summary>
[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
}
}

130
src/LibHac/Fs/GameCard.cs Normal file
View File

@ -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<byte> 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
}
}

View File

@ -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 = "");
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace LibHac.Fs
{
public interface ICommonMountNameGenerator
{
Result Generate(Span<byte> nameBuffer);
}
}

View File

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System;
namespace LibHac.Fs
{
@ -8,32 +8,24 @@ namespace LibHac.Fs
public interface IDirectory
{
/// <summary>
/// The <see cref="IFileSystem"/> that contains the current <see cref="IDirectory"/>.
/// Retrieves the next entries that this directory contains. Does not search subdirectories.
/// </summary>
IFileSystem ParentFileSystem { get; }
/// <param name="entriesRead">The number of <see cref="DirectoryEntry"/>s that
/// were read into <paramref name="entryBuffer"/>.</param>
/// <param name="entryBuffer">The buffer the entries will be read into.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>With each call of <see cref="Read"/>, the <see cref="IDirectory"/> 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 <see cref="Read"/> will
/// read 0 entries into the buffer.</remarks>
Result Read(out long entriesRead, Span<DirectoryEntry> entryBuffer);
/// <summary>
/// The full path of the current <see cref="IDirectory"/> in its <see cref="ParentFileSystem"/>.
/// Retrieves the number of file system entries that this directory contains. Does not search subdirectories.
/// </summary>
string FullPath { get; }
/// <summary>
/// Specifies which types of entries will be enumerated when <see cref="Read"/> is called.
/// </summary>
OpenDirectoryMode Mode { get; }
/// <summary>
/// Returns an enumerable collection the file system entries of the types specified by
/// <see cref="Mode"/> that this directory contains. Does not search subdirectories.
/// </summary>
/// <returns>An enumerable collection of file system entries in this directory.</returns>
IEnumerable<DirectoryEntry> Read();
/// <summary>
/// Returns the number of file system entries of the types specified by
/// <see cref="Mode"/> that this directory contains. Does not search subdirectories.
/// </summary>
/// <returns>The number of child entries the directory contains.</returns>
int GetEntryCount();
/// <param name="entryCount">The number of child entries the directory contains.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result GetEntryCount(out long entryCount);
}
}

View File

@ -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 <see cref="Write"/> is called on an offset past the end of the <see cref="IFile"/>,
/// the <see cref="OpenMode.Append"/> mode is set and the file supports expansion,
/// the <see cref="OpenMode.AllowAppend"/> mode is set and the file supports expansion,
/// the file will be expanded so that it is large enough to contain the written data.</remarks>
public interface IFile : IDisposable
{
/// <summary>
/// The permissions mode for the current file.
/// </summary>
OpenMode Mode { get; }
/// <summary>
/// Reads a sequence of bytes from the current <see cref="IFile"/>.
/// </summary>
/// <param name="bytesRead">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.</param>
/// <param name="offset">The offset in the <see cref="IFile"/> at which to begin reading.</param>
/// <param name="destination">The buffer where the read bytes will be stored.
/// The number of bytes read will be no larger than the length of the buffer.</param>
/// <param name="offset">The offset in the <see cref="IFile"/> at which to begin reading.</param>
/// <param name="options">Options for reading from the <see cref="IFile"/>.</param>
/// <returns>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.</returns>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> is invalid.</exception>
/// <exception cref="NotSupportedException">The file's <see cref="OpenMode"/> does not allow reading.</exception>
int Read(Span<byte> destination, long offset, ReadOption options);
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result Read(out long bytesRead, long offset, Span<byte> destination, ReadOption options);
/// <summary>
/// Writes a sequence of bytes to the current <see cref="IFile"/>.
/// </summary>
/// <param name="source">The buffer containing the bytes to be written.</param>
/// <param name="offset">The offset in the <see cref="IStorage"/> at which to begin writing.</param>
/// <param name="source">The buffer containing the bytes to be written.</param>
/// <param name="options">Options for writing to the <see cref="IFile"/>.</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="offset"/> is negative.</exception>
/// <exception cref="NotSupportedException">The file's <see cref="OpenMode"/> does not allow this request.</exception>
void Write(ReadOnlySpan<byte> source, long offset, WriteOption options);
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result Write(long offset, ReadOnlySpan<byte> source, WriteOption options);
/// <summary>
/// Causes any buffered data to be written to the underlying device.
/// </summary>
void Flush();
/// <summary>
/// Gets the number of bytes in the file.
/// </summary>
/// <returns>The length of the file in bytes.</returns>
long GetSize();
Result Flush();
/// <summary>
/// Sets the size of the file in bytes.
/// </summary>
/// <param name="size">The desired size of the file in bytes.</param>
/// <exception cref="NotSupportedException">If increasing the file size, The file's
/// <see cref="OpenMode"/> does not allow this appending.</exception>
void SetSize(long size);
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result SetSize(long size);
/// <summary>
/// Gets the number of bytes in the file.
/// </summary>
/// <param name="size">If the operation returns successfully, the length of the file in bytes.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result GetSize(out long size);
/// <summary>
/// Performs various operations on the file. Used to extend the functionality of the <see cref="IFile"/> interface.
/// </summary>
/// <param name="outBuffer">A buffer that will contain the response from the operation.</param>
/// <param name="operationId">The operation to be performed.</param>
/// <param name="offset">The offset of the range to operate on.</param>
/// <param name="size">The size of the range to operate on.</param>
/// <param name="inBuffer">An input buffer. Size may vary depending on the operation performed.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result OperateRange(Span<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> inBuffer);
}
}

View File

@ -1,4 +1,5 @@
using System;
using LibHac.FsSystem;
namespace LibHac.Fs
{
@ -7,19 +8,6 @@ namespace LibHac.Fs
/// </summary>
public interface IFileSystem
{
/// <summary>
/// Creates all directories and subdirectories in the specified path unless they already exist.
/// </summary>
/// <param name="path">The full path of the directory to create.</param>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The parent directory of the specified path does not exist: <see cref="ResultFs.PathNotFound"/>
/// Specified path already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// Insufficient free space to create the directory: <see cref="ResultFs.InsufficientFreeSpace"/>
/// </remarks>
void CreateDirectory(string path);
/// <summary>
/// Creates or overwrites a file at the specified path.
/// </summary>
@ -27,162 +15,191 @@ namespace LibHac.Fs
/// <param name="size">The initial size of the created file.</param>
/// <param name="options">Flags to control how the file is created.
/// Should usually be <see cref="CreateFileOptions.None"/></param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The parent directory of the specified path does not exist: <see cref="ResultFs.PathNotFound"/>
/// Specified path already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// Insufficient free space to create the file: <see cref="ResultFs.InsufficientFreeSpace"/>
/// </remarks>
void CreateFile(string path, long size, CreateFileOptions options);
/// <summary>
/// Deletes the specified directory.
/// </summary>
/// <param name="path">The full path of the directory to delete.</param>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// The specified directory is not empty: <see cref="ResultFs.DirectoryNotEmpty"/>
/// </remarks>
void DeleteDirectory(string path);
/// <summary>
/// Deletes the specified directory and any subdirectories and files in the directory.
/// </summary>
/// <param name="path">The full path of the directory to delete.</param>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
void DeleteDirectoryRecursively(string path);
/// <summary>
/// Deletes any subdirectories and files in the specified directory.
/// </summary>
/// <param name="path">The full path of the directory to clean.</param>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
void CleanDirectoryRecursively(string path);
Result CreateFile(string path, long size, CreateFileOptions options);
/// <summary>
/// Deletes the specified file.
/// </summary>
/// <param name="path">The full path of the file to delete.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The specified path does not exist or is a directory: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
void DeleteFile(string path);
Result DeleteFile(string path);
/// <summary>
/// Creates an <see cref="IDirectory"/> instance for enumerating the specified directory.
/// Creates all directories and subdirectories in the specified path unless they already exist.
/// </summary>
/// <param name="path">The directory's full path.</param>
/// <param name="mode">Specifies which sub-entries should be enumerated.</param>
/// <returns>An <see cref="IDirectory"/> instance for the specified directory.</returns>
/// <param name="path">The full path of the directory to create.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The parent directory of the specified path does not exist: <see cref="ResultFs.PathNotFound"/>
/// Specified path already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// Insufficient free space to create the directory: <see cref="ResultFs.InsufficientFreeSpace"/>
/// </remarks>
Result CreateDirectory(string path);
/// <summary>
/// Deletes the specified directory.
/// </summary>
/// <param name="path">The full path of the directory to delete.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// The specified directory is not empty: <see cref="ResultFs.DirectoryNotEmpty"/>
/// </remarks>
Result DeleteDirectory(string path);
/// <summary>
/// Deletes the specified directory and any subdirectories and files in the directory.
/// </summary>
/// <param name="path">The full path of the directory to delete.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
IDirectory OpenDirectory(string path, OpenDirectoryMode mode);
Result DeleteDirectoryRecursively(string path);
/// <summary>
/// Opens an <see cref="IFile"/> instance for the specified path.
/// Deletes any subdirectories and files in the specified directory.
/// </summary>
/// <param name="path">The full path of the file to open.</param>
/// <param name="mode">Specifies the access permissions of the created <see cref="IFile"/>.</param>
/// <returns>An <see cref="IFile"/> instance for the specified path.</returns>
/// <param name="path">The full path of the directory to clean.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The specified path does not exist or is a directory: <see cref="ResultFs.PathNotFound"/>
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
IFile OpenFile(string path, OpenMode mode);
/// <summary>
/// Renames or moves a directory to a new location.
/// </summary>
/// <param name="srcPath">The full path of the directory to rename.</param>
/// <param name="dstPath">The new full path of the directory.</param>
/// <returns>An <see cref="IFile"/> instance for the specified path.</returns>
/// <remarks>
/// If <paramref name="srcPath"/> and <paramref name="dstPath"/> are the same, this function does nothing and returns successfully.
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
///
/// <paramref name="srcPath"/> does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="dstPath"/>'s parent directory does not exist: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="dstPath"/> already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// Either <paramref name="srcPath"/> or <paramref name="dstPath"/> is a subpath of the other: <see cref="ResultFs.DestinationIsSubPathOfSource"/>
/// </remarks>
void RenameDirectory(string srcPath, string dstPath);
Result CleanDirectoryRecursively(string path);
/// <summary>
/// Renames or moves a file to a new location.
/// </summary>
/// <param name="srcPath">The full path of the file to rename.</param>
/// <param name="dstPath">The new full path of the file.</param>
/// <param name="oldPath">The full path of the file to rename.</param>
/// <param name="newPath">The new full path of the file.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// If <paramref name="srcPath"/> and <paramref name="dstPath"/> are the same, this function does nothing and returns successfully.
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
/// If <paramref name="oldPath"/> and <paramref name="newPath"/> are the same, this function does nothing and returns successfully.
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// <paramref name="srcPath"/> does not exist or is a directory: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="dstPath"/>'s parent directory does not exist: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="dstPath"/> already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// <paramref name="oldPath"/> does not exist or is a directory: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="newPath"/>'s parent directory does not exist: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="newPath"/> already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// </remarks>
void RenameFile(string srcPath, string dstPath);
Result RenameFile(string oldPath, string newPath);
/// <summary>
/// Renames or moves a directory to a new location.
/// </summary>
/// <param name="oldPath">The full path of the directory to rename.</param>
/// <param name="newPath">The new full path of the directory.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// If <paramref name="oldPath"/> and <paramref name="newPath"/> are the same, this function does nothing and returns <see cref="Result.Success"/>.
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// <paramref name="oldPath"/> does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="newPath"/>'s parent directory does not exist: <see cref="ResultFs.PathNotFound"/>
/// <paramref name="newPath"/> already exists as either a file or directory: <see cref="ResultFs.PathAlreadyExists"/>
/// Either <paramref name="oldPath"/> or <paramref name="newPath"/> is a subpath of the other: <see cref="ResultFs.DestinationIsSubPathOfSource"/>
/// </remarks>
Result RenameDirectory(string oldPath, string newPath);
/// <summary>
/// Determines whether the specified path is a file or directory, or does not exist.
/// </summary>
/// <param name="entryType">If the operation returns successfully, the <see cref="DirectoryEntryType"/> of the file.</param>
/// <param name="path">The full path to check.</param>
/// <returns>The <see cref="DirectoryEntryType"/> of the file.</returns>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// This function operates slightly differently than it does in Horizon OS.
/// Instead of returning <see cref="ResultFs.PathNotFound"/> when an entry is missing,
/// the function will return <see cref="DirectoryEntryType.NotFound"/>.
/// </remarks>
DirectoryEntryType GetEntryType(string path);
Result GetEntryType(out DirectoryEntryType entryType, string path);
/// <summary>
/// Gets the amount of available free space on a drive, in bytes.
/// </summary>
/// <param name="freeSpace">If the operation returns successfully, the amount of free space available on the drive, in bytes.</param>
/// <param name="path">The path of the drive to query. Unused in almost all cases.</param>
/// <returns>The amount of free space available on the drive, in bytes.</returns>
long GetFreeSpaceSize(string path);
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result GetFreeSpaceSize(out long freeSpace, string path);
/// <summary>
/// Gets the total size of storage space on a drive, in bytes.
/// </summary>
/// <param name="totalSpace">If the operation returns successfully, the total size of the drive, in bytes.</param>
/// <param name="path">The path of the drive to query. Unused in almost all cases.</param>
/// <returns>The total size of the drive, in bytes.</returns>
long GetTotalSpaceSize(string path);
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result GetTotalSpaceSize(out long totalSpace, string path);
/// <summary>
/// Gets the creation, last accessed, and last modified timestamps of a file or directory.
/// Opens an <see cref="IFile"/> instance for the specified path.
/// </summary>
/// <param name="path">The path of the file or directory.</param>
/// <returns>The timestamps for the specified file or directory.
/// This value is expressed as a Unix timestamp</returns>
/// <param name="file">If the operation returns successfully,
/// An <see cref="IFile"/> instance for the specified path.</param>
/// <param name="path">The full path of the file to open.</param>
/// <param name="mode">Specifies the access permissions of the created <see cref="IFile"/>.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// A <see cref="HorizonResultException"/> will be thrown with the given <see cref="Result"/> under the following conditions:
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The specified path does not exist: <see cref="ResultFs.PathNotFound"/>
/// The specified path does not exist or is a directory: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
FileTimeStampRaw GetFileTimeStampRaw(string path);
Result OpenFile(out IFile file, string path, OpenMode mode);
/// <summary>
/// Creates an <see cref="IDirectory"/> instance for enumerating the specified directory.
/// </summary>
/// <param name="directory">If the operation returns successfully,
/// An <see cref="IDirectory"/> instance for the specified directory.</param>
/// <param name="path">The directory's full path.</param>
/// <param name="mode">Specifies which sub-entries should be enumerated.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The specified path does not exist or is a file: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
Result OpenDirectory(out IDirectory directory, string path, OpenDirectoryMode mode);
/// <summary>
/// Commits any changes to a transactional file system.
/// Does nothing if called on a non-transactional file system.
/// </summary>
void Commit();
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result Commit();
/// <summary>
/// Gets the creation, last accessed, and last modified timestamps of a file or directory.
/// </summary>
/// <param name="timeStamp">If the operation returns successfully, the timestamps for the specified file or directory.
/// These value are expressed as Unix timestamps.</param>
/// <param name="path">The path of the file or directory.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
/// <remarks>
/// The following <see cref="Result"/> codes may be returned under certain conditions:
///
/// The specified path does not exist: <see cref="ResultFs.PathNotFound"/>
/// </remarks>
Result GetFileTimeStampRaw(out FileTimeStampRaw timeStamp, string path);
/// <summary>
/// Performs a query on the specified file.
@ -193,9 +210,10 @@ namespace LibHac.Fs
/// May be unused depending on the query type.</param>
/// <param name="inBuffer">The buffer for sending data to the query operation.
/// May be unused depending on the query type.</param>
/// <param name="path">The full path of the file to query.</param>
/// <param name="queryId">The type of query to perform.</param>
void QueryEntry(Span<byte> outBuffer, ReadOnlySpan<byte> inBuffer, string path, QueryId queryId);
/// <param name="path">The full path of the file to query.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result QueryEntry(Span<byte> outBuffer, ReadOnlySpan<byte> inBuffer, QueryId queryId, string path);
}
/// <summary>
@ -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
}
/// <summary>

View File

@ -14,7 +14,7 @@ namespace LibHac.Fs
/// The number of bytes read will be equal to the length of the buffer.</param>
/// <param name="offset">The offset in the <see cref="IStorage"/> at which to begin reading.</param>
/// <exception cref="ArgumentException">Invalid offset or the IStorage contains fewer bytes than requested. </exception>
void Read(Span<byte> destination, long offset);
Result Read(long offset, Span<byte> destination);
/// <summary>
/// Writes a sequence of bytes to the current <see cref="IStorage"/>.
@ -23,24 +23,36 @@ namespace LibHac.Fs
/// <param name="offset">The offset in the <see cref="IStorage"/> at which to begin writing.</param>
/// <exception cref="ArgumentException">Invalid offset or <paramref name="source"/>
/// is too large to be written to the IStorage. </exception>
void Write(ReadOnlySpan<byte> source, long offset);
Result Write(long offset, ReadOnlySpan<byte> source);
/// <summary>
/// Causes any buffered data to be written to the underlying device.
/// </summary>
void Flush();
Result Flush();
/// <summary>
/// Sets the size of the current IStorage.
/// </summary>
/// <param name="size">The desired size of the current IStorage in bytes.</param>
void SetSize(long size);
Result SetSize(long size);
/// <summary>
/// The size of the<see cref="IStorage"/>. -1 will be returned if
/// the <see cref="IStorage"/> cannot be represented as a sequence of contiguous bytes.
/// </summary>
/// <returns>The size of the <see cref="IStorage"/> in bytes.</returns>
long GetSize();
Result GetSize(out long size);
/// <summary>
/// Performs various operations on the file. Used to extend the functionality of the <see cref="IStorage"/> interface.
/// </summary>
/// <param name="outBuffer">A buffer that will contain the response from the operation.</param>
/// <param name="operationId">The operation to be performed.</param>
/// <param name="offset">The offset of the range to operate on.</param>
/// <param name="size">The size of the range to operate on.</param>
/// <param name="inBuffer">An input buffer. Size may vary depending on the operation performed.</param>
/// <returns>The <see cref="Result"/> of the requested operation.</returns>
Result OperateRange(Span<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> inBuffer);
}
}

View File

@ -1,133 +0,0 @@
using System;
using System.Collections.Generic;
namespace LibHac.Fs
{
public class LayeredFileSystem : IFileSystem
{
private List<IFileSystem> Sources { get; } = new List<IFileSystem>();
public LayeredFileSystem(IList<IFileSystem> sourceFileSystems)
{
Sources.AddRange(sourceFileSystems);
}
public IDirectory OpenDirectory(string path, OpenDirectoryMode mode)
{
path = PathTools.Normalize(path);
var dirs = new List<IDirectory>();
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<byte> outBuffer, ReadOnlySpan<byte> 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;
}
}
}

View File

@ -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<IDirectory> Sources { get; }
public LayeredFileSystemDirectory(IFileSystem fs, List<IDirectory> sources, string path, OpenDirectoryMode mode)
{
ParentFileSystem = fs;
Sources = sources;
FullPath = path;
Mode = mode;
}
public IEnumerable<DirectoryEntry> Read()
{
var returnedFiles = new HashSet<string>();
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();
}
}
}

View File

@ -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<DirectoryEntry> 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;
}
}
}

View File

@ -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<byte> 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<byte> 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;
}
}
}
}

View File

@ -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; }
/// <summary>
/// Opens a directory on local storage as an <see cref="IFileSystem"/>.
/// The directory will be created if it does not exist.
/// </summary>
/// <param name="basePath">The path that will be the root of the <see cref="LocalFileSystem"/>.</param>
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<byte> outBuffer, ReadOnlySpan<byte> 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);
}
}
}

View File

@ -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<byte> destination, long offset)
{
Storage.Read(destination, offset);
}
protected override void WriteImpl(ReadOnlySpan<byte> source, long offset)
{
Storage.Write(source, offset);
}
public override void Flush()
{
Storage.Flush();
}
public override long GetSize() => Storage.GetSize();
}
}

View File

@ -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;
}
}
}

View File

@ -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<byte> destination, long offset, ReadOption options)
{
int toRead = ValidateReadParamsAndGetSize(destination, offset);
destination.Slice(0, toRead).Clear();
return toRead;
}
public override void Write(ReadOnlySpan<byte> source, long offset, WriteOption options)
{
}
public override void Flush()
{
}
public override long GetSize() => Length;
public override void SetSize(long size)
{
throw new NotSupportedException();
}
}
}

View File

@ -1,30 +0,0 @@
using System;
namespace LibHac.Fs
{
/// <summary>
/// An <see cref="IStorage"/> that returns all zeros when read, and does nothing on write.
/// </summary>
public class NullStorage : StorageBase
{
public NullStorage() { }
public NullStorage(long length) => _length = length;
private long _length;
protected override void ReadImpl(Span<byte> destination, long offset)
{
destination.Clear();
}
protected override void WriteImpl(ReadOnlySpan<byte> source, long offset)
{
}
public override void Flush()
{
}
public override long GetSize() => _length;
}
}

View File

@ -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<DirectoryEntry> 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;
}
}
}

View File

@ -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<byte> 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<byte> 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);
}
}
}

View File

@ -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<DirectoryEntry> Read() => BaseDir.Read();
public int GetEntryCount() => BaseDir.GetEntryCount();
}
}

View File

@ -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<byte> 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<byte> source, long offset, WriteOption options) =>
ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFile);
public override void SetSize(long size) =>
ThrowHelper.ThrowResult(ResultFs.UnsupportedOperationModifyReadOnlyFile);
}
}

View File

@ -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<byte> outBuffer, ReadOnlySpan<byte> 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);
}
}

View File

@ -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);
}
}

62
src/LibHac/Fs/RightsId.cs Normal file
View File

@ -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<RightsId>, IComparable<RightsId>, IComparable
{
public readonly Id128 Id;
public RightsId(ulong high, ulong low)
{
Id = new Id128(high, low);
}
public RightsId(ReadOnlySpan<byte> uid)
{
Id = new Id128(uid);
}
public override string ToString() => Id.ToString();
public string DebugDisplay()
{
ReadOnlySpan<byte> highBytes = AsBytes().Slice(0, 8);
ReadOnlySpan<byte> 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<byte> output) => Id.ToBytes(output);
public ReadOnlySpan<byte> 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;
}
}

View File

@ -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<DirectoryEntry> Read()
{
FindPosition position = InitialPosition;
HierarchicalRomFileTable<RomFileInfo> 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<RomFileInfo> 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;
}
}
}

View File

@ -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<byte> 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<byte> 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);
}
}
}

View File

@ -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<DirectoryEntry> 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;
}
}
}

View File

@ -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<byte> destination, long offset, ReadOption options)
{
int toRead = ValidateReadParamsAndGetSize(destination, offset);
BaseStorage.Read(destination.Slice(0, toRead), offset);
return toRead;
}
public override void Write(ReadOnlySpan<byte> 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;
}
}
}

108
src/LibHac/Fs/SaveData.cs Normal file
View File

@ -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();
}
}
}

View File

@ -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<SaveDataStruct>, IComparable, IEquatable<SaveDataStruct>, IExportable
public class SaveDataAttributeKvdb : IComparable<SaveDataAttributeKvdb>, IComparable, IEquatable<SaveDataAttributeKvdb>, 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)}");
}
}
}

View File

@ -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<byte> 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;
}
}

View File

@ -0,0 +1,16 @@
using System;
using LibHac.FsService;
namespace LibHac.Fs
{
/// <summary>
/// The default access logger that will output to the SD card via <see cref="FileSystemProxy"/>.
/// </summary>
public class SdCardAccessLog : IAccessLog
{
public void Log(Result result, TimeSpan startTime, TimeSpan endTime, int handleId, string message, string caller = "")
{
throw new NotImplementedException();
}
}
}

View File

@ -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<byte> destination, long offset)
{
ValidateSize(destination.Length, offset);
BaseStorage.Read(destination, offset);
}
protected override void WriteImpl(ReadOnlySpan<byte> 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();
}
/// <summary>
/// Validates that the size is a multiple of the sector size
/// </summary>
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}");
}
}
}

View File

@ -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<IDisposable> ToDispose { get; } = new List<IDisposable>();
protected bool CanAutoExpand { get; set; }
// 0 = not disposed; 1 = disposed
private int _disposedState;
private bool IsDisposed => _disposedState != 0;
protected abstract void ReadImpl(Span<byte> destination, long offset);
protected abstract void WriteImpl(ReadOnlySpan<byte> source, long offset);
public abstract void Flush();
public abstract long GetSize();
protected abstract Result ReadImpl(long offset, Span<byte> destination);
protected abstract Result WriteImpl(long offset, ReadOnlySpan<byte> source);
protected abstract Result FlushImpl();
protected abstract Result GetSizeImpl(out long size);
protected abstract Result SetSizeImpl(long size);
public void Read(Span<byte> destination, long offset)
protected virtual Result OperateRangeImpl(Span<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> inBuffer)
{
ValidateParameters(destination, offset);
ReadImpl(destination, offset);
return ResultFs.NotImplemented.Log();
}
public void Write(ReadOnlySpan<byte> source, long offset)
public Result Read(long offset, Span<byte> 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<byte> 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<byte> outBuffer, OperationId operationId, long offset, long size,
ReadOnlySpan<byte> 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<byte> 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;
}
}
}

View File

@ -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<byte> destination, long offset, ReadOption options)
{
int toRead = ValidateReadParamsAndGetSize(destination, offset);
BaseStorage.Read(destination.Slice(0, toRead), offset);
return toRead;
}
public override void Write(ReadOnlySpan<byte> 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);
}
}
}

View File

@ -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<byte> 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<byte> 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;
}
}
}

View File

@ -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<byte> 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<byte> 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;
}
}
}

View File

@ -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<byte> outBuffer, ReadOnlySpan<byte> inBuffer, string path, QueryId queryId)
{
path = PathTools.Normalize(path);
ParentFileSystem.QueryEntry(outBuffer, inBuffer, ResolveFullPath(path), queryId);
}
}
}

View File

@ -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<DirectoryEntry> 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();
}
}
}

View File

@ -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<UserId>, IComparable<UserId>, 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<byte> uid)
{
ReadOnlySpan<ulong> longs = MemoryMarshal.Cast<byte, ulong>(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<byte> output)
{
Span<ulong> longs = MemoryMarshal.Cast<byte, ulong>(output);
public void ToBytes(Span<byte> output) => Id.ToBytes(output);
longs[0] = High;
longs[1] = Low;
public ReadOnlySpan<byte> 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;
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<byte> deviceId = stackalloc byte[0x10];
Span<byte> 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<byte> deviceId, ReadOnlySpan<byte> imageHash)
{
GameCard = gameCard;
Handle = handle;
IsSecureMode = true;
deviceId.CopyTo(DeviceId);
imageHash.CopyTo(ImageHash);
}
protected override Result ReadImpl(long offset, Span<byte> 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<byte> 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;
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<byte> 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;
}
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,9 @@
using LibHac.Fs;
namespace LibHac.FsService.Creators
{
public interface IBuiltInStorageCreator
{
Result Create(out IStorage storage, BisPartitionId partitionId);
}
}

View File

@ -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);
}
}

View File

@ -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<byte> encryptionSeed);
}
public enum EncryptedFsKeyId
{
Save = 0,
Content = 1,
CustomStorage = 2
}
}

View File

@ -0,0 +1,9 @@
using LibHac.Fs;
namespace LibHac.FsService.Creators
{
public interface IFatFileSystemCreator
{
Result Create(out IFileSystem fileSystem, IStorage baseStorage);
}
}

View File

@ -0,0 +1,9 @@
using LibHac.Fs;
namespace LibHac.FsService.Creators
{
public interface IGameCardFileSystemCreator
{
Result Create(out IFileSystem fileSystem, GameCardHandle handle, GameCardPartition partitionType);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show More