mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-05-21 12:07:58 +00:00
[SonyPFS] Implement directory operations.
This commit is contained in:
283
Aaru.Filesystems/SonyPFS/Dir.cs
Normal file
283
Aaru.Filesystems/SonyPFS/Dir.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
// /***************************************************************************
|
||||
// Aaru Data Preservation Suite
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// Filename : Dir.cs
|
||||
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// ----------------------------------------------------------------------------
|
||||
// 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
|
||||
{
|
||||
/// <summary>Reads directory entries from an inode, returning a dictionary of name → DirEntry.</summary>
|
||||
Dictionary<string, DirEntry> ReadDirectory(Inode dirInode)
|
||||
{
|
||||
Dictionary<string, DirEntry> 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;
|
||||
}
|
||||
|
||||
/// <summary>Parses directory entries from a metadata chunk.</summary>
|
||||
void ParseDentryChunk(byte[] data, uint chunkOffset, ulong dirSize, ref ulong bytesRead,
|
||||
Dictionary<string, DirEntry> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Wraps the data array index past PFS_INODE_MAX_BLOCKS using modulo 123.</summary>
|
||||
static uint FixIndex(uint index)
|
||||
{
|
||||
if(index < PFS_INODE_MAX_BLOCKS)
|
||||
return index;
|
||||
|
||||
return (index - PFS_INODE_MAX_BLOCKS) % 123;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<string, DirEntry> 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<string, DirEntry> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ErrorNumber CloseDir(IDirNode node)
|
||||
{
|
||||
if(node is not PfsDirNode mynode)
|
||||
return ErrorNumber.InvalidArgument;
|
||||
|
||||
mynode.Position = -1;
|
||||
mynode.Contents = null;
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
}
|
||||
108
Aaru.Filesystems/SonyPFS/Inode.cs
Normal file
108
Aaru.Filesystems/SonyPFS/Inode.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
// /***************************************************************************
|
||||
// Aaru Data Preservation Suite
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// Filename : Inode.cs
|
||||
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// ----------------------------------------------------------------------------
|
||||
// Copyright © 2011-2026 Natalia Portillo
|
||||
// ****************************************************************************/
|
||||
|
||||
using System;
|
||||
using Aaru.CommonTypes.Enums;
|
||||
using Aaru.Helpers;
|
||||
|
||||
namespace Aaru.Filesystems;
|
||||
|
||||
public partial class SonyPFS
|
||||
{
|
||||
/// <summary>Reads an inode from the image at the specified block number and sub-partition.</summary>
|
||||
/// <param name="blockNumber">Inode block number.</param>
|
||||
/// <param name="subpart">Sub-partition (only 0/main is supported).</param>
|
||||
/// <param name="inode">The deserialized inode structure.</param>
|
||||
/// <returns><see cref="ErrorNumber.NoError" /> on success.</returns>
|
||||
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<Inode>();
|
||||
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<Inode>(data);
|
||||
|
||||
if(inode.magic != PFS_SEGD_MAGIC && inode.magic != PFS_SEGI_MAGIC)
|
||||
return ErrorNumber.InvalidArgument;
|
||||
|
||||
return ErrorNumber.NoError;
|
||||
}
|
||||
|
||||
/// <summary>Reads a data block from the image.</summary>
|
||||
/// <param name="bi">Block info describing the location.</param>
|
||||
/// <param name="offsetInZone">Block offset within the zone run.</param>
|
||||
/// <param name="data">The raw data read (one zone worth).</param>
|
||||
/// <returns><see cref="ErrorNumber.NoError" /> on success.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Converts a PFS DateTime to a .NET DateTimeOffset.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Aaru.Filesystems/SonyPFS/Internal.cs
Normal file
90
Aaru.Filesystems/SonyPFS/Internal.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
// /***************************************************************************
|
||||
// Aaru Data Preservation Suite
|
||||
// ----------------------------------------------------------------------------
|
||||
//
|
||||
// Filename : Internal.cs
|
||||
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// ----------------------------------------------------------------------------
|
||||
// Copyright © 2011-2026 Natalia Portillo
|
||||
// ****************************************************************************/
|
||||
|
||||
using Aaru.CommonTypes.Interfaces;
|
||||
|
||||
namespace Aaru.Filesystems;
|
||||
|
||||
public partial class SonyPFS
|
||||
{
|
||||
#region Nested type: DirEntry
|
||||
|
||||
/// <summary>Cached directory entry used for in-memory directory tree.</summary>
|
||||
sealed class DirEntry
|
||||
{
|
||||
/// <summary>Inode number.</summary>
|
||||
public uint Inode;
|
||||
/// <summary>Sub-partition index.</summary>
|
||||
public ushort SubPart;
|
||||
/// <summary>File type from directory entry aLen upper bits.</summary>
|
||||
public ushort Mode;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nested type: PfsDirNode
|
||||
|
||||
/// <summary>Directory node for PFS directory enumeration.</summary>
|
||||
sealed class PfsDirNode : IDirNode
|
||||
{
|
||||
internal string[] Contents;
|
||||
internal int Position;
|
||||
|
||||
#region IDirNode Members
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Path { get; init; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Nested type: PfsFileNode
|
||||
|
||||
/// <summary>File node for PFS file reading.</summary>
|
||||
sealed class PfsFileNode : IFileNode
|
||||
{
|
||||
internal Inode InodeData;
|
||||
internal uint InodeNumber;
|
||||
internal ushort SubPart;
|
||||
|
||||
#region IFileNode Members
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Path { get; init; }
|
||||
/// <inheritdoc />
|
||||
public long Length { get; init; }
|
||||
/// <inheritdoc />
|
||||
public long Offset { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user