mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:17 +00:00
589 lines
22 KiB
C#
589 lines
22 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : File.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : SmartFileSystem 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 Aaru.CommonTypes.Enums;
|
|
using Aaru.CommonTypes.Interfaces;
|
|
using Aaru.CommonTypes.Structs;
|
|
using Aaru.Helpers;
|
|
|
|
namespace Aaru.Filesystems;
|
|
|
|
/// <inheritdoc />
|
|
public sealed partial class SFS
|
|
{
|
|
/// <summary>Amiga epoch: January 1, 1978</summary>
|
|
static readonly DateTime _amigaEpoch = new(1978, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadLink(string path, out string dest)
|
|
{
|
|
dest = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Normalize the path
|
|
string normalizedPath = path ?? "";
|
|
|
|
if(normalizedPath == "" || normalizedPath == "." || normalizedPath == "/") return ErrorNumber.InvalidArgument;
|
|
|
|
// Get file stat to validate it's a symlink
|
|
ErrorNumber errno = Stat(normalizedPath, out FileEntryInfo stat);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Verify it's a symbolic link
|
|
if(!stat.Attributes.HasFlag(FileAttributes.Symlink)) return ErrorNumber.InvalidArgument;
|
|
|
|
// Get the object node for this path
|
|
errno = GetObjectNodeForPath(normalizedPath, out uint objectNode);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Find the object container for this node
|
|
errno = FindObjectNode(objectNode, out uint objectBlock);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
errno = ReadBlock(objectBlock, out byte[] objectData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Find the object in the container
|
|
errno = FindObjectInContainer(objectData, objectNode, out int objectOffset);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Read the softlink block pointer (data field at offset 12)
|
|
// For soft links, the data field points to a SOFTLINK_ID block
|
|
var softLinkBlock = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 12);
|
|
|
|
if(softLinkBlock == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
// Read the soft link block
|
|
errno = ReadBlock(softLinkBlock, out byte[] linkData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Validate block ID
|
|
var blockId = BigEndianBitConverter.ToUInt32(linkData, 0);
|
|
|
|
if(blockId != SOFTLINK_ID) return ErrorNumber.InvalidArgument;
|
|
|
|
// SoftLink structure: header (12) + parent (4) + next (4) + previous (4) = 24 bytes
|
|
// Followed by null-terminated path string
|
|
const int stringOffset = 24;
|
|
|
|
// Read null-terminated string
|
|
var pathBytes = new List<byte>();
|
|
|
|
for(int i = stringOffset; i < linkData.Length && linkData[i] != 0; i++) pathBytes.Add(linkData[i]);
|
|
|
|
if(pathBytes.Count == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
dest = _encoding.GetString(pathBytes.ToArray());
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber OpenFile(string path, out IFileNode node)
|
|
{
|
|
node = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Normalize the path
|
|
string normalizedPath = path ?? "";
|
|
|
|
if(normalizedPath == "" || normalizedPath == "." || normalizedPath == "/") return ErrorNumber.IsDirectory;
|
|
|
|
// Get file stat to validate it's a file and get its size
|
|
ErrorNumber errno = Stat(normalizedPath, out FileEntryInfo stat);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Verify it's a regular file, not a directory
|
|
if(stat.Attributes.HasFlag(FileAttributes.Directory)) return ErrorNumber.IsDirectory;
|
|
|
|
// Get the object node for this path
|
|
errno = GetObjectNodeForPath(normalizedPath, out uint objectNode);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Find the object container for this node
|
|
errno = FindObjectNode(objectNode, out uint objectBlock);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
errno = ReadBlock(objectBlock, out byte[] objectData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Find the object in the container
|
|
errno = FindObjectInContainer(objectData, objectNode, out int objectOffset);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Read the first extent block (data field at offset 12)
|
|
var firstExtent = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 12);
|
|
|
|
// Create file node
|
|
node = new SfsFileNode
|
|
{
|
|
Path = normalizedPath,
|
|
Length = stat.Length,
|
|
Offset = 0,
|
|
FirstExtent = firstExtent,
|
|
CurrentExtent = firstExtent,
|
|
ExtentOffset = 0
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber CloseFile(IFileNode node)
|
|
{
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
if(node is not SfsFileNode) return ErrorNumber.InvalidArgument;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read)
|
|
{
|
|
read = 0;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
if(buffer is null || buffer.Length < length) return ErrorNumber.InvalidArgument;
|
|
|
|
if(node is not SfsFileNode sfsNode) return ErrorNumber.InvalidArgument;
|
|
|
|
if(sfsNode.Offset < 0 || length < 0) return ErrorNumber.InvalidArgument;
|
|
|
|
// If at or past end of file, return zero bytes read
|
|
if(sfsNode.Offset >= sfsNode.Length) return ErrorNumber.NoError;
|
|
|
|
// Adjust length to not read past end of file
|
|
long bytesToRead = length;
|
|
|
|
if(sfsNode.Offset + bytesToRead > sfsNode.Length) bytesToRead = sfsNode.Length - sfsNode.Offset;
|
|
|
|
if(bytesToRead == 0) return ErrorNumber.NoError;
|
|
|
|
// Seek to the correct extent if needed
|
|
ErrorNumber errno = SeekToOffset(sfsNode);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Read data from extent chain
|
|
long bufferOffset = 0;
|
|
|
|
while(bytesToRead > 0 && sfsNode.CurrentExtent != 0)
|
|
{
|
|
// Find the current extent B-node
|
|
errno = FindExtentBNode(sfsNode.CurrentExtent,
|
|
out uint extentKey,
|
|
out uint extentNext,
|
|
out uint extentBlocks);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Calculate how many bytes are available in this extent
|
|
long extentSize = (long)extentBlocks << _blockShift;
|
|
long bytesInExtent = extentSize - sfsNode.ExtentOffset;
|
|
long bytesFromExtent = Math.Min(bytesToRead, bytesInExtent);
|
|
|
|
// Calculate starting block and offset within the extent
|
|
long offsetInExtent = sfsNode.ExtentOffset;
|
|
uint startBlock = extentKey + (uint)(offsetInExtent >> _blockShift);
|
|
var offsetInBlock = (int)(offsetInExtent & _blockSize - 1);
|
|
|
|
// Read blocks
|
|
while(bytesFromExtent > 0)
|
|
{
|
|
errno = ReadBlock(startBlock, out byte[] blockData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
var bytesFromBlock = (int)Math.Min(bytesFromExtent, _blockSize - offsetInBlock);
|
|
Array.Copy(blockData, offsetInBlock, buffer, bufferOffset, bytesFromBlock);
|
|
|
|
bufferOffset += bytesFromBlock;
|
|
bytesFromExtent -= bytesFromBlock;
|
|
bytesToRead -= bytesFromBlock;
|
|
read += bytesFromBlock;
|
|
sfsNode.Offset += bytesFromBlock;
|
|
sfsNode.ExtentOffset += bytesFromBlock;
|
|
|
|
offsetInBlock = 0;
|
|
startBlock++;
|
|
}
|
|
|
|
// Move to next extent if we've exhausted this one
|
|
if(sfsNode.ExtentOffset >= extentSize && extentNext != 0)
|
|
{
|
|
sfsNode.CurrentExtent = extentNext;
|
|
sfsNode.ExtentOffset = 0;
|
|
}
|
|
}
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ErrorNumber Stat(string path, out FileEntryInfo stat)
|
|
{
|
|
stat = null;
|
|
|
|
if(!_mounted) return ErrorNumber.AccessDenied;
|
|
|
|
// Normalize the path
|
|
string normalizedPath = path ?? "/";
|
|
|
|
if(normalizedPath == "" || normalizedPath == ".") normalizedPath = "/";
|
|
|
|
// Root directory handling
|
|
if(normalizedPath == "/" || string.Equals(normalizedPath, "/", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Read root object to get its metadata
|
|
ErrorNumber errno = FindObjectNode(ROOTNODE, out uint rootBlock);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
errno = ReadBlock(rootBlock, out byte[] rootData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
errno = FindObjectInContainer(rootData, ROOTNODE, out int rootOffset);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
return StatFromObjectData(rootData, rootOffset, ROOTNODE, out stat);
|
|
}
|
|
|
|
// Remove leading slash and split path
|
|
string pathWithoutLeadingSlash = normalizedPath.StartsWith("/", StringComparison.Ordinal)
|
|
? normalizedPath[1..]
|
|
: normalizedPath;
|
|
|
|
string[] pathComponents = pathWithoutLeadingSlash.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if(pathComponents.Length == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
// Start traversal from root directory cache
|
|
Dictionary<string, uint> currentDirectory = _rootDirectoryCache;
|
|
uint targetNode = 0;
|
|
|
|
// Traverse all path components
|
|
for(var i = 0; i < pathComponents.Length; i++)
|
|
{
|
|
string component = pathComponents[i];
|
|
|
|
// Find the component in current directory (handle case sensitivity)
|
|
string foundKey = null;
|
|
|
|
foreach(string key in currentDirectory.Keys)
|
|
{
|
|
if(string.Equals(key,
|
|
component,
|
|
_caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foundKey = key;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(foundKey == null) return ErrorNumber.NoSuchFile;
|
|
|
|
targetNode = currentDirectory[foundKey];
|
|
|
|
// If this is the last component, we found our target
|
|
if(i == pathComponents.Length - 1) break;
|
|
|
|
// Not the last component - read directory contents for next iteration
|
|
ErrorNumber errno = ReadDirectoryContents(targetNode, out Dictionary<string, uint> childEntries);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
currentDirectory = childEntries;
|
|
}
|
|
|
|
// Read the target object's metadata
|
|
ErrorNumber findErr = FindObjectNode(targetNode, out uint objectBlock);
|
|
|
|
if(findErr != ErrorNumber.NoError) return findErr;
|
|
|
|
findErr = ReadBlock(objectBlock, out byte[] objectData);
|
|
|
|
if(findErr != ErrorNumber.NoError) return findErr;
|
|
|
|
findErr = FindObjectInContainer(objectData, targetNode, out int objectOffset);
|
|
|
|
if(findErr != ErrorNumber.NoError) return findErr;
|
|
|
|
return StatFromObjectData(objectData, objectOffset, targetNode, out stat);
|
|
}
|
|
|
|
/// <summary>Seeks to the correct extent for the current file offset</summary>
|
|
/// <param name="node">The file node</param>
|
|
/// <returns>Error code indicating success or failure</returns>
|
|
ErrorNumber SeekToOffset(SfsFileNode node)
|
|
{
|
|
// If we're before the current extent position, restart from the beginning
|
|
long currentExtentStart = node.Offset - node.ExtentOffset;
|
|
|
|
if(node.Offset < currentExtentStart || node.CurrentExtent == 0)
|
|
{
|
|
node.CurrentExtent = node.FirstExtent;
|
|
node.ExtentOffset = 0;
|
|
currentExtentStart = 0;
|
|
}
|
|
|
|
// Walk the extent chain to find the correct extent
|
|
while(node.CurrentExtent != 0)
|
|
{
|
|
ErrorNumber errno = FindExtentBNode(node.CurrentExtent, out _, out uint extentNext, out uint extentBlocks);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
long extentSize = (long)extentBlocks << _blockShift;
|
|
long extentEnd = currentExtentStart + extentSize;
|
|
|
|
if(node.Offset < extentEnd)
|
|
{
|
|
// Found the correct extent
|
|
node.ExtentOffset = node.Offset - currentExtentStart;
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
// Move to next extent
|
|
currentExtentStart = extentEnd;
|
|
node.CurrentExtent = extentNext;
|
|
node.ExtentOffset = 0;
|
|
}
|
|
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
/// <summary>Finds an extent B-node by key in the extent B-tree</summary>
|
|
/// <param name="key">The extent key (block number)</param>
|
|
/// <param name="extentKey">Output: the extent's data block start</param>
|
|
/// <param name="extentNext">Output: the next extent in the chain</param>
|
|
/// <param name="extentBlocks">Output: number of blocks in this extent</param>
|
|
/// <returns>Error code indicating success or failure</returns>
|
|
ErrorNumber FindExtentBNode(uint key, out uint extentKey, out uint extentNext, out uint extentBlocks)
|
|
{
|
|
extentKey = 0;
|
|
extentNext = 0;
|
|
extentBlocks = 0;
|
|
|
|
// Start at the extent B-tree root
|
|
uint currentBlock = _rootBlock.extentbnoderoot;
|
|
|
|
while(true)
|
|
{
|
|
ErrorNumber errno = ReadBlock(currentBlock, out byte[] blockData);
|
|
|
|
if(errno != ErrorNumber.NoError) return errno;
|
|
|
|
// Validate block ID
|
|
var blockId = BigEndianBitConverter.ToUInt32(blockData, 0);
|
|
|
|
if(blockId != BNODECONTAINER_ID) return ErrorNumber.InvalidArgument;
|
|
|
|
// BTreeContainer starts at offset 12 (after header)
|
|
// nodecount (2) + isleaf (1) + nodesize (1)
|
|
var nodeCount = BigEndianBitConverter.ToUInt16(blockData, 12);
|
|
byte isLeaf = blockData[14];
|
|
byte nodeSize = blockData[15];
|
|
|
|
if(nodeCount == 0) return ErrorNumber.InvalidArgument;
|
|
|
|
// Search for the key in this container
|
|
// BNode entries start at offset 16
|
|
var nodeOffset = 16;
|
|
|
|
if(isLeaf != 0)
|
|
{
|
|
// Leaf node - contains ExtentBNode structures
|
|
// SFS\0: key (4) + next (4) + prev (4) + blocks (2) = 14 bytes
|
|
// SFS\2: key (4) + next (4) + prev (4) + blocks (4) = 16 bytes
|
|
// nodeSize field tells us the actual size
|
|
for(var i = 0; i < nodeCount; i++)
|
|
{
|
|
var nodeKey = BigEndianBitConverter.ToUInt32(blockData, nodeOffset);
|
|
|
|
if(nodeKey == key)
|
|
{
|
|
extentKey = nodeKey;
|
|
extentNext = BigEndianBitConverter.ToUInt32(blockData, nodeOffset + 4);
|
|
|
|
// Read blocks field - 16-bit in SFS\0 (nodeSize=14), 32-bit in SFS\2 (nodeSize=16)
|
|
extentBlocks = nodeSize >= 16
|
|
? BigEndianBitConverter.ToUInt32(blockData, nodeOffset + 12)
|
|
: BigEndianBitConverter.ToUInt16(blockData, nodeOffset + 12);
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
|
|
nodeOffset += nodeSize;
|
|
}
|
|
|
|
// Key not found
|
|
return ErrorNumber.InvalidArgument;
|
|
}
|
|
|
|
// Index node - find the child to descend into
|
|
// BNode: key (4) + data (4) = 8 bytes
|
|
uint childBlock = 0;
|
|
|
|
for(int i = nodeCount - 1; i >= 0; i--)
|
|
{
|
|
int entryOffset = nodeOffset + i * nodeSize;
|
|
var nodeKey = BigEndianBitConverter.ToUInt32(blockData, entryOffset);
|
|
|
|
if(key >= nodeKey)
|
|
{
|
|
childBlock = BigEndianBitConverter.ToUInt32(blockData, entryOffset + 4);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(childBlock == 0)
|
|
{
|
|
// Use first entry
|
|
childBlock = BigEndianBitConverter.ToUInt32(blockData, nodeOffset + 4);
|
|
}
|
|
|
|
currentBlock = childBlock;
|
|
}
|
|
}
|
|
|
|
/// <summary>Creates a FileEntryInfo from raw object data in an ObjectContainer</summary>
|
|
/// <param name="objectData">The ObjectContainer block data</param>
|
|
/// <param name="objectOffset">Offset to the object within the container</param>
|
|
/// <param name="objectNode">The object's node number</param>
|
|
/// <param name="stat">Output file entry information</param>
|
|
/// <returns>Error code indicating success or failure</returns>
|
|
ErrorNumber StatFromObjectData(byte[] objectData, int objectOffset, uint objectNode, out FileEntryInfo stat)
|
|
{
|
|
stat = null;
|
|
|
|
// Object structure (from objects.h):
|
|
// SFS\0 (25 bytes):
|
|
// owneruid (2) + ownergid (2) + objectnode (4) + protection (4) +
|
|
// data/hashtable (4) + size/firstdirblock (4) + datemodified (4) + bits (1)
|
|
// SFS\2 (27 bytes):
|
|
// owneruid (2) + ownergid (2) + objectnode (4) + protection (4) +
|
|
// data/hashtable (4) + size/firstdirblock (4) + sizeh (2) + datemodified (4) + bits (1)
|
|
|
|
if(objectOffset + _objectSize > objectData.Length) return ErrorNumber.InvalidArgument;
|
|
|
|
var ownerUid = BigEndianBitConverter.ToUInt16(objectData, objectOffset);
|
|
var ownerGid = BigEndianBitConverter.ToUInt16(objectData, objectOffset + 2);
|
|
var protection = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 8);
|
|
var dataOrHash = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 12);
|
|
var sizeOrDir = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 16);
|
|
|
|
ushort sizeh;
|
|
uint dateModified;
|
|
ObjectBits bits;
|
|
|
|
if(_isSfs2)
|
|
{
|
|
// SFS\2: sizeh at offset 20, datemodified at 22, bits at 26
|
|
sizeh = BigEndianBitConverter.ToUInt16(objectData, objectOffset + 20);
|
|
dateModified = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 22);
|
|
bits = (ObjectBits)objectData[objectOffset + 26];
|
|
}
|
|
else
|
|
{
|
|
// SFS\0: datemodified at offset 20, bits at 24
|
|
sizeh = 0;
|
|
dateModified = BigEndianBitConverter.ToUInt32(objectData, objectOffset + 20);
|
|
bits = (ObjectBits)objectData[objectOffset + 24];
|
|
}
|
|
|
|
// Determine file attributes
|
|
FileAttributes attributes = FileAttributes.None;
|
|
|
|
if((bits & ObjectBits.Directory) != 0)
|
|
attributes |= FileAttributes.Directory;
|
|
else
|
|
attributes |= FileAttributes.File;
|
|
|
|
if((bits & ObjectBits.Hidden) != 0) attributes |= FileAttributes.Hidden;
|
|
|
|
if((bits & ObjectBits.Link) != 0 && (bits & ObjectBits.HardLink) == 0) attributes |= FileAttributes.Symlink;
|
|
|
|
// SFS protection bits: opposite of AmigaDOS
|
|
// Default is 0x0000000F (R, W, E, D set)
|
|
// If write bit (bit 1) is NOT set, file is read-only
|
|
if((protection & 0x02) == 0) attributes |= FileAttributes.ReadOnly;
|
|
|
|
// Calculate file size and blocks
|
|
long length = 0;
|
|
long blocks = 0;
|
|
|
|
if((bits & ObjectBits.Directory) == 0)
|
|
{
|
|
// In SFS\2, file size is 48-bit: (sizeOrDir << 16) | sizeh
|
|
// sizeh contains the LOW 16 bits, sizeOrDir contains the HIGH 32 bits
|
|
length = _isSfs2 ? (long)sizeOrDir << 16 | sizeh : sizeOrDir;
|
|
blocks = (length + _blockSize - 1) / _blockSize;
|
|
}
|
|
|
|
// Convert SFS timestamp (seconds since 1-1-1978) to DateTime
|
|
DateTime? lastWriteTimeUtc = _amigaEpoch.AddSeconds(dateModified);
|
|
|
|
stat = new FileEntryInfo
|
|
{
|
|
Attributes = attributes,
|
|
Inode = objectNode,
|
|
Length = length,
|
|
Blocks = blocks,
|
|
BlockSize = _blockSize,
|
|
Links = 1, // SFS doesn't track hard link count in the object
|
|
UID = ownerUid,
|
|
GID = ownerGid,
|
|
Mode = protection,
|
|
LastWriteTimeUtc = lastWriteTimeUtc
|
|
};
|
|
|
|
return ErrorNumber.NoError;
|
|
}
|
|
} |