From 7b4998413c497f78ac1d290e079d0d5df27268b3 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Mon, 13 Apr 2026 11:30:23 +0100 Subject: [PATCH] [SonyPFS] Implement directory operations. --- Aaru.Filesystems/SonyPFS/Dir.cs | 283 +++++++++++++++++++++++++++ Aaru.Filesystems/SonyPFS/Inode.cs | 108 ++++++++++ Aaru.Filesystems/SonyPFS/Internal.cs | 90 +++++++++ 3 files changed, 481 insertions(+) create mode 100644 Aaru.Filesystems/SonyPFS/Dir.cs create mode 100644 Aaru.Filesystems/SonyPFS/Inode.cs create mode 100644 Aaru.Filesystems/SonyPFS/Internal.cs diff --git a/Aaru.Filesystems/SonyPFS/Dir.cs b/Aaru.Filesystems/SonyPFS/Dir.cs new file mode 100644 index 000000000..ad11dbb27 --- /dev/null +++ b/Aaru.Filesystems/SonyPFS/Dir.cs @@ -0,0 +1,283 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Dir.cs +// Author(s) : Natalia Portillo +// +// Component : PlayStation FileSystem plugin. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Helpers; + +namespace Aaru.Filesystems; + +public partial class SonyPFS +{ + /// Reads directory entries from an inode, returning a dictionary of name → DirEntry. + Dictionary ReadDirectory(Inode dirInode) + { + Dictionary entries = new(); + + // Directory data starts at data[1]; data[0] is the inode's own block + // Walk through all data segments + uint zoneSize = _superBlock.zone_size; + + // We need to follow the inode's data array and possibly indirect segment descriptors + // For simplicity, read from the direct inode first + Inode currentSegment = dirInode; + uint dataIndex = 1; // Start from data[1] + uint totalDataBlocks = dirInode.number_data; + ulong bytesRead = 0; + + while(dataIndex < totalDataBlocks) + { + uint fixedIndex = FixIndex(dataIndex); + + // If fixedIndex wrapped to 0 and we're past the direct blocks, + // we need to follow the next segment descriptor + if(fixedIndex == 0 && dataIndex >= PFS_INODE_MAX_BLOCKS) + { + if(currentSegment.next_segment.number == 0) + break; + + ErrorNumber errno = ReadInode(currentSegment.next_segment.number, + currentSegment.next_segment.subpart, + out currentSegment); + + if(errno != ErrorNumber.NoError) + break; + + // After loading next segment, fixedIndex 0 is the segment's own block, skip to next + dataIndex++; + + continue; + } + + BlockInfo bi = currentSegment.data[fixedIndex]; + + // Read each zone in this block run + for(uint offset = 0; offset < bi.count && bytesRead < dirInode.size; offset++) + { + ErrorNumber errno = ReadDataBlock(bi, offset, out byte[] zoneData); + + if(errno != ErrorNumber.NoError) + return entries; + + // Parse directory entries from the zone data + // Each metadata chunk is PFS_META_SIZE (1024) bytes + uint chunksPerZone = zoneSize / PFS_META_SIZE; + + for(uint chunk = 0; chunk < chunksPerZone && bytesRead < dirInode.size; chunk++) + { + uint chunkOffset = chunk * PFS_META_SIZE; + + if(chunkOffset + 8 > zoneData.Length) + break; + + // Parse directory entries within this 512-byte sector-aligned area + // Entries are parsed in 512-byte sectors within the 1024-byte metadata chunk + ParseDentryChunk(zoneData, chunkOffset, dirInode.size, ref bytesRead, entries); + } + } + + dataIndex++; + } + + return entries; + } + + /// Parses directory entries from a metadata chunk. + void ParseDentryChunk(byte[] data, uint chunkOffset, ulong dirSize, ref ulong bytesRead, + Dictionary entries) + { + uint pos = chunkOffset; + uint end = chunkOffset + PFS_META_SIZE; + + while(pos < end && pos + 8 <= (uint)data.Length && bytesRead < dirSize) + { + uint inode = BitConverter.ToUInt32(data, (int)pos); + byte sub = data[pos + 4]; + byte pLen = data[pos + 5]; + ushort aLen = BitConverter.ToUInt16(data, (int)(pos + 6)); + + uint allocLen = (uint)(aLen & 0x0FFF); + var mode = (ushort)(aLen & 0xF000); + + if(allocLen == 0 || (allocLen & 3) != 0) + break; + + if(pos + allocLen > end) + break; + + bytesRead += allocLen; + + if(pLen > 0 && inode != 0 && pos + 8 + pLen <= (uint)data.Length) + { + string name = Encoding.ASCII.GetString(data, (int)(pos + 8), pLen); + + if(name != "." && name != ".." && !entries.ContainsKey(name)) + { + entries[name] = new DirEntry + { + Inode = inode, + SubPart = sub, + Mode = mode + }; + } + } + + pos += allocLen; + } + } + + /// Wraps the data array index past PFS_INODE_MAX_BLOCKS using modulo 123. + static uint FixIndex(uint index) + { + if(index < PFS_INODE_MAX_BLOCKS) + return index; + + return (index - PFS_INODE_MAX_BLOCKS) % 123; + } + + /// + public ErrorNumber OpenDir(string path, out IDirNode node) + { + node = null; + + if(!_mounted) + return ErrorNumber.AccessDenied; + + if(string.IsNullOrWhiteSpace(path) || path == "/") + { + node = new PfsDirNode + { + Path = path, + Contents = _rootDirectoryCache.Keys.ToArray(), + Position = 0 + }; + + return ErrorNumber.NoError; + } + + string cutPath = path.StartsWith("/", StringComparison.Ordinal) + ? path[1..].ToLower(CultureInfo.CurrentUICulture) + : path.ToLower(CultureInfo.CurrentUICulture); + + if(_directoryCache.TryGetValue(cutPath, out Dictionary currentDirectory)) + { + node = new PfsDirNode + { + Path = path, + Contents = currentDirectory.Keys.ToArray(), + Position = 0 + }; + + return ErrorNumber.NoError; + } + + string[] pieces = cutPath.Split(['/'], StringSplitOptions.RemoveEmptyEntries); + + currentDirectory = _rootDirectoryCache; + string currentPath = ""; + + for(var p = 0; p < pieces.Length; p++) + { + KeyValuePair entry = + currentDirectory.FirstOrDefault(t => t.Key.Equals(pieces[p], + StringComparison.CurrentCultureIgnoreCase)); + + if(string.IsNullOrEmpty(entry.Key)) + return ErrorNumber.NoSuchFile; + + if((entry.Value.Mode & (ushort)FileType.IFMT) != (ushort)FileType.IFDIR) + return ErrorNumber.NotDirectory; + + currentPath = p == 0 ? pieces[0] : $"{currentPath}/{pieces[p]}"; + + if(_directoryCache.TryGetValue(currentPath, out currentDirectory)) + continue; + + // Read the inode for this subdirectory + ErrorNumber errno = ReadInode(entry.Value.Inode, entry.Value.SubPart, out Inode dirInode); + + if(errno != ErrorNumber.NoError) + return errno; + + currentDirectory = ReadDirectory(dirInode); + + _directoryCache[currentPath] = currentDirectory; + } + + if(currentDirectory is null) + return ErrorNumber.NoSuchFile; + + node = new PfsDirNode + { + Path = path, + Contents = currentDirectory.Keys.ToArray(), + Position = 0 + }; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadDir(IDirNode node, out string filename) + { + filename = null; + + if(!_mounted) + return ErrorNumber.AccessDenied; + + if(node is not PfsDirNode mynode) + return ErrorNumber.InvalidArgument; + + if(mynode.Position < 0) + return ErrorNumber.InvalidArgument; + + if(mynode.Position >= mynode.Contents.Length) + return ErrorNumber.NoError; + + filename = mynode.Contents[mynode.Position++]; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber CloseDir(IDirNode node) + { + if(node is not PfsDirNode mynode) + return ErrorNumber.InvalidArgument; + + mynode.Position = -1; + mynode.Contents = null; + + return ErrorNumber.NoError; + } +} \ No newline at end of file diff --git a/Aaru.Filesystems/SonyPFS/Inode.cs b/Aaru.Filesystems/SonyPFS/Inode.cs new file mode 100644 index 000000000..1f908d006 --- /dev/null +++ b/Aaru.Filesystems/SonyPFS/Inode.cs @@ -0,0 +1,108 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Inode.cs +// Author(s) : Natalia Portillo +// +// Component : PlayStation FileSystem plugin. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using System; +using Aaru.CommonTypes.Enums; +using Aaru.Helpers; + +namespace Aaru.Filesystems; + +public partial class SonyPFS +{ + /// Reads an inode from the image at the specified block number and sub-partition. + /// Inode block number. + /// Sub-partition (only 0/main is supported). + /// The deserialized inode structure. + /// on success. + ErrorNumber ReadInode(uint blockNumber, ushort subpart, out Inode inode) + { + inode = default; + + // Only main partition is accessible through the image + if(subpart != 0) + return ErrorNumber.InvalidArgument; + + // Calculate the sector offset: blockNumber << inode_scale gives the block in zone units, + // then multiply by sectors per zone to get sector offset + ulong sectorOffset = (ulong)(blockNumber << (int)_inodeScale) * _sectorsPerZone; + ulong sector = _partitionStart + sectorOffset; + + int inodeSize = Marshal.SizeOf(); + var sectorsToRead = (uint)((inodeSize + _sectorSize - 1) / _sectorSize); + + ErrorNumber errno = _image.ReadSectors(sector, false, sectorsToRead, out byte[] data, out _); + + if(errno != ErrorNumber.NoError) + return errno; + + if(data.Length < inodeSize) + return ErrorNumber.InvalidArgument; + + inode = Marshal.ByteArrayToStructureLittleEndian(data); + + if(inode.magic != PFS_SEGD_MAGIC && inode.magic != PFS_SEGI_MAGIC) + return ErrorNumber.InvalidArgument; + + return ErrorNumber.NoError; + } + + /// Reads a data block from the image. + /// Block info describing the location. + /// Block offset within the zone run. + /// The raw data read (one zone worth). + /// on success. + ErrorNumber ReadDataBlock(BlockInfo bi, uint offsetInZone, out byte[] data) + { + data = null; + + if(bi.subpart != 0) + return ErrorNumber.InvalidArgument; + + ulong sectorOffset = (ulong)((bi.number + offsetInZone) << (int)_inodeScale) * _sectorsPerZone; + ulong sector = _partitionStart + sectorOffset; + + ErrorNumber errno = _image.ReadSectors(sector, false, _sectorsPerZone, out data, out _); + + return errno; + } + + /// Converts a PFS DateTime to a .NET DateTimeOffset. + static DateTimeOffset PfsDateTimeToDateTimeOffset(DateTime pfsDateTime) + { + try + { + return new DateTimeOffset(pfsDateTime.year, pfsDateTime.month, pfsDateTime.day, + pfsDateTime.hour, pfsDateTime.min, pfsDateTime.sec, + TimeSpan.Zero); + } + catch + { + return DateTimeOffset.MinValue; + } + } +} \ No newline at end of file diff --git a/Aaru.Filesystems/SonyPFS/Internal.cs b/Aaru.Filesystems/SonyPFS/Internal.cs new file mode 100644 index 000000000..1a9c7d35e --- /dev/null +++ b/Aaru.Filesystems/SonyPFS/Internal.cs @@ -0,0 +1,90 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Internal.cs +// Author(s) : Natalia Portillo +// +// Component : PlayStation FileSystem plugin. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// ****************************************************************************/ + +using Aaru.CommonTypes.Interfaces; + +namespace Aaru.Filesystems; + +public partial class SonyPFS +{ +#region Nested type: DirEntry + + /// Cached directory entry used for in-memory directory tree. + sealed class DirEntry + { + /// Inode number. + public uint Inode; + /// Sub-partition index. + public ushort SubPart; + /// File type from directory entry aLen upper bits. + public ushort Mode; + } + +#endregion + +#region Nested type: PfsDirNode + + /// Directory node for PFS directory enumeration. + sealed class PfsDirNode : IDirNode + { + internal string[] Contents; + internal int Position; + + #region IDirNode Members + + /// + public string Path { get; init; } + + #endregion + } + +#endregion + +#region Nested type: PfsFileNode + + /// File node for PFS file reading. + sealed class PfsFileNode : IFileNode + { + internal Inode InodeData; + internal uint InodeNumber; + internal ushort SubPart; + + #region IFileNode Members + + /// + public string Path { get; init; } + /// + public long Length { get; init; } + /// + public long Offset { get; set; } + + #endregion + } + +#endregion +} \ No newline at end of file