Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Library/DiscUtils.SquashFs/ExtendedSymlinkInode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Copyright (c) 2008-2011, Kenneth Bell
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//


namespace DiscUtils.SquashFs;

/// <summary>
/// An extended symbolic link inode (type 10). Its fixed part is the basic symlink inode's: the common header, the
/// link count and the target length. The target path follows, as in the basic form, and then a 32-bit index into
/// the xattr table, which the basic form does not have. mksquashfs writes this form for a symlink that carries
/// extended attributes.
/// </summary>
internal sealed class ExtendedSymlinkInode : SymlinkInode
{
/// <summary>The xattr table index stored after the target path (0xFFFFFFFF for none); read along with the target.</summary>
public uint XattrIndex { get; internal set; } = uint.MaxValue;
}
3 changes: 3 additions & 0 deletions Library/DiscUtils.SquashFs/File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public File(Context context, Inode inode, MetadataRef inodeRef)

protected Context Context { get; }

/// <summary>Where the inode sits in the inode table; what follows the inode (a symlink's target, a file's block sizes) is read from here.</summary>
protected MetadataRef InodeRef => _inodeRef;

internal Inode Inode { get; }

public DateTime LastAccessTimeUtc
Expand Down
36 changes: 16 additions & 20 deletions Library/DiscUtils.SquashFs/FileContentBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ internal class FileContentBuffer : Streams.Buffer
private const uint InvalidFragmentKey = 0xFFFFFFFF;

private readonly int[] _blockLengths;
private readonly long[] _blockStarts;
private readonly Context _context;
private readonly RegularInode _inode;

Expand Down Expand Up @@ -64,6 +65,17 @@ public FileContentBuffer(Context context, RegularInode inode, MetadataRef inodeR
_blockLengths[i] = EndianUtilities.ToInt32LittleEndian(lengthData.Slice(i * 4, 4));
}
}

// Where each block starts on disk: the blocks lie one after another from the inode's start, each as long
// as the low 24 bits of its size field say (a sparse block takes no room). Computed once, so a read at any
// position finds its block without walking the sizes from the first block.
_blockStarts = new long[numBlocks];
var start = _inode.StartBlock;
for (var i = 0; i < numBlocks; ++i)
{
_blockStarts[i] = start;
start += _blockLengths[i] & 0x00FFFFFF;
}
}

public override bool CanRead => true;
Expand All @@ -86,8 +98,6 @@ public override int Read(long pos, byte[] buffer, int offset, int count)
var currentPos = pos;
var totalRead = 0;
var totalToRead = (int)Math.Min(_inode.FileSize - pos, count);
var currentBlock = 0;
long currentBlockDiskStart = _inode.StartBlock;
while (totalRead < totalToRead)
{
if (currentPos >= startOfFragment)
Expand All @@ -97,13 +107,7 @@ public override int Read(long pos, byte[] buffer, int offset, int count)
return totalRead + read;
}

var targetBlock = (int)(currentPos / _context.SuperBlock.BlockSize);
while (currentBlock < targetBlock)
{
currentBlockDiskStart += _blockLengths[currentBlock] & 0x7FFFFF;
++currentBlock;
}

var currentBlock = (int)(currentPos / _context.SuperBlock.BlockSize);
var blockOffset = (int)(currentPos % _context.SuperBlock.BlockSize);
if ((_blockLengths[currentBlock] & 0x00FFFFFF) == 0)
{
Expand All @@ -115,7 +119,7 @@ public override int Read(long pos, byte[] buffer, int offset, int count)
continue;
}

var block = _context.ReadBlock(currentBlockDiskStart, _blockLengths[currentBlock]);
var block = _context.ReadBlock(_blockStarts[currentBlock], _blockLengths[currentBlock]);

var toCopy = Math.Min(block.Available - blockOffset, totalToRead - totalRead);
System.Buffer.BlockCopy(block.Data, blockOffset, buffer, offset + totalRead, toCopy);
Expand Down Expand Up @@ -148,8 +152,6 @@ public override int Read(long pos, Span<byte> buffer)
var currentPos = pos;
var totalRead = 0;
var totalToRead = (int)Math.Min(_inode.FileSize - pos, buffer.Length);
var currentBlock = 0;
long currentBlockDiskStart = _inode.StartBlock;
while (totalRead < totalToRead)
{
if (currentPos >= startOfFragment)
Expand All @@ -158,13 +160,7 @@ public override int Read(long pos, Span<byte> buffer)
return totalRead + read;
}

var targetBlock = (int)(currentPos / _context.SuperBlock.BlockSize);
while (currentBlock < targetBlock)
{
currentBlockDiskStart += _blockLengths[currentBlock] & 0x7FFFFF;
++currentBlock;
}

var currentBlock = (int)(currentPos / _context.SuperBlock.BlockSize);
var blockOffset = (int)(currentPos % _context.SuperBlock.BlockSize);
if ((_blockLengths[currentBlock] & 0x00FFFFFF) == 0)
{
Expand All @@ -176,7 +172,7 @@ public override int Read(long pos, Span<byte> buffer)
continue;
}

var block = _context.ReadBlock(currentBlockDiskStart, _blockLengths[currentBlock]);
var block = _context.ReadBlock(_blockStarts[currentBlock], _blockLengths[currentBlock]);

var toCopy = Math.Min(block.Available - blockOffset, totalToRead - totalRead);
block.Data.AsSpan(blockOffset, toCopy).CopyTo(buffer[totalRead..]);
Expand Down
1 change: 1 addition & 0 deletions Library/DiscUtils.SquashFs/Inode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ private static Inode InstantiateType(InodeType type)
InodeType.File => new RegularInode(),
InodeType.ExtendedFile => new ExtendedFileInode(),
InodeType.Symlink => new SymlinkInode(),
InodeType.ExtendedSymlink => new ExtendedSymlinkInode(),
InodeType.CharacterDevice or InodeType.BlockDevice => new DeviceInode(),
_ => throw new NotImplementedException($"Inode type not implemented: {type}"),
};
Expand Down
3 changes: 3 additions & 0 deletions Library/DiscUtils.SquashFs/MetablockReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public MetablockReader(Context context, long start)
_start = start;
}

/// <summary>Where the metadata block the reader is in starts in the image, in bytes.</summary>
public long CurrentBlockStart => _start + _currentBlockStart;

public void SetPosition(MetadataRef position)
{
SetPosition(position.Block, position.Offset);
Expand Down
55 changes: 52 additions & 3 deletions Library/DiscUtils.SquashFs/Symlink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,64 @@
// DEALINGS IN THE SOFTWARE.
//

using System;

using System.IO;
using System.Text;
using DiscUtils.Vfs;

namespace DiscUtils.SquashFs;

internal class Symlink : File, IVfsSymlink<DirectoryEntry, File>
{
/// <summary>
/// The format's maximum target length: SQUASHFS_SYMLINK_MAX in squashfs_fs.h (squashfs-tools and the Linux
/// driver), 65535 bytes. mksquashfs enforces it when it reads a link (a 65536-byte readlink buffer, and a
/// result that fills it is refused), so no well-formed image stores a longer target.
/// </summary>
public const int MaxTargetLength = 65535;

private string _targetPath;

public Symlink(Context context, Inode inode, MetadataRef inodeRef)
: base(context, inode, inodeRef) { }

public string TargetPath => throw new NotImplementedException();
}
/// <summary>
/// The link's target as stored: the UTF-8 path that follows the inode in the inode table, relative to the
/// link's directory unless it starts with a slash. The length comes from the image, so it is checked against
/// <see cref="MaxTargetLength"/> before anything is allocated, and the bytes must lie within the inode table,
/// which the directory table follows directly. An extended inode keeps its xattr index after the target.
/// </summary>
public string TargetPath
{
get
{
if (_targetPath is null)
{
var inode = (SymlinkInode)Inode;
if (inode.SymlinkSize > MaxTargetLength)
{
throw new IOException($"Corrupt symlink inode: target length {inode.SymlinkSize} exceeds {MaxTargetLength}");
}

var reader = Context.InodeReader;
reader.SetPosition(InodeRef);
reader.Skip(inode.Size);
var target = new byte[inode.SymlinkSize];
var read = reader.Read(target);
if (inode is ExtendedSymlinkInode extended)
{
extended.XattrIndex = reader.ReadUInt();
}

if (read != target.Length || reader.CurrentBlockStart >= Context.SuperBlock.DirectoryTableStart)
{
throw new IOException("Corrupt symlink inode: target runs past the inode table");
}

_targetPath = Encoding.UTF8.GetString(target);
}

return _targetPath;
}
}
}
8 changes: 3 additions & 5 deletions Library/DiscUtils.SquashFs/VfsSquashFileSystemReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,8 @@ public VfsSquashFileSystemReader(Stream stream, SquashFileSystemReaderOptions op
throw new IOException($"Unsupported compression {_context.SuperBlock.Compression} used");
}

if (_context.SuperBlock.ExtendedAttrsTableStart != -1)
{
throw new IOException("Unsupported extended attributes present");
}
// An extended attribute table is left alone: the reader exposes no attributes, and the inodes of the files
// that carry them (the extended forms) are read like the others, their attribute index unused.

if (_context.SuperBlock.MajorVersion != 4)
{
Expand Down Expand Up @@ -122,7 +120,7 @@ public VfsSquashFileSystemReader(Stream stream, SquashFileSystemReaderOptions op

// Bootstrap the root directory
_context.InodeReader.SetPosition(_context.SuperBlock.RootInode);
var dirInode = (DirectoryInode)Inode.Read(_context.InodeReader);
var dirInode = Inode.Read(_context.InodeReader);
RootDirectory = new Directory(_context, dirInode, _context.SuperBlock.RootInode);
}

Expand Down
2 changes: 2 additions & 0 deletions Tests/LibraryTests/LibraryTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
<EmbeddedResource Include="ExFat\Environment\exFAT.vhdx.gz" />
<EmbeddedResource Include="SquashFs\extended-inodes.sqsh" />
<EmbeddedResource Include="SquashFs\huge-sparse.sqsh" />
<EmbeddedResource Include="SquashFs\big-root.sqsh" />
<EmbeddedResource Include="SquashFs\xattr-symlink.sqsh" />
</ItemGroup>

<ItemGroup>
Expand Down
87 changes: 87 additions & 0 deletions Tests/LibraryTests/SquashFs/SquashFileSystemReaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

using System;
using System.IO;
using System.Linq;
using System.Text;
using DiscUtils.SquashFs;

Expand Down Expand Up @@ -132,6 +133,92 @@ public void SparseFileLargerThan4GiB()
Assert.Equal(0, stream.Read(tail, 0, tail.Length));
}

/// <summary>
/// mksquashfs stores a directory whose table exceeds one metadata block as an extended directory inode (with an
/// index), the root included; the reader used to cast the root to the basic inode type. big-root.sqsh holds
/// 1,000 files and a symlink in its root.
/// </summary>
[Fact]
public void RootStoredAsExtendedDirectoryInode()
{
using var fs = OpenImage("big-root.sqsh");
Assert.Equal(1001, fs.GetFileSystemEntries("").Count());
Assert.True(fs.FileExists("entry-0999"));
Assert.Equal(0, fs.GetFileLength("entry-0999"));
}

/// <summary>
/// A symbolic link's target is the path stored after its inode; a lookup through the link reaches the target
/// (the reader used to throw NotImplementedException from any path that landed on a symlink).
/// </summary>
[Fact]
public void SymlinkTargetIsRead()
{
using var fs = OpenImage("big-root.sqsh");
Assert.True(fs.FileExists("link"));
Assert.Equal("first", ReadText(fs, "link"));
Assert.Equal(5, fs.GetFileLength("link"));
}

/// <summary>
/// A symlink that carries an extended attribute is stored as an extended symlink inode: the fixed part and the
/// target path of the basic form, then a 32-bit xattr index after the path. xattr-symlink.sqsh was made with
/// -xattrs-add, which puts an attribute on every inode, so its link is of that kind, and the image has an xattr
/// table (checked here from the superblock), which the reader used to refuse outright.
/// </summary>
[Fact]
public void ExtendedSymlinkInodeOfALinkWithAttributes()
{
using var image = SquashFixtures.Open("xattr-symlink.sqsh");
var superblock = new byte[96];
Assert.Equal(superblock.Length, image.Read(superblock, 0, superblock.Length));
Assert.NotEqual(-1L, BitConverter.ToInt64(superblock, 56));
image.Position = 0;

using var fs = new SquashFileSystemReader(image);
Assert.Equal("hello", ReadText(fs, "target.txt"));
Assert.True(fs.FileExists("link"));
Assert.Equal("hello", ReadText(fs, "link"));
}

/// <summary>
/// mixed.bin has, in order, a compressed block, a block stored as is (incompressible), a sparse block (all
/// zeros, taking no room on disk), another compressed block and a 100-byte tail in a fragment. Each block's
/// start on disk is the sum of the stored sizes before it, a sparse block counting nothing, so a read across
/// every boundary and a read of the whole file must come back exact.
/// </summary>
[Fact]
public void BlocksCompressedStoredSparseAndFragmentInOneFile()
{
var expected = SquashFixtures.Mixed;
using var fs = OpenImage("extended-inodes.sqsh");
Assert.Equal(expected.Length, fs.GetFileLength("mixed.bin"));
using var stream = fs.OpenFile("mixed.bin", FileMode.Open, FileAccess.Read);

const int block = 128 * 1024;
foreach (var offset in new[] { block - 50, 2 * block - 50, 3 * block - 50, 4 * block - 50 })
{
var piece = new byte[100];
stream.Position = offset;
Assert.Equal(piece.Length, stream.Read(piece, 0, piece.Length));
var want = new byte[piece.Length];
Array.Copy(expected, offset, want, 0, want.Length);
Assert.Equal(want, piece);
}

stream.Position = 0;
var all = new byte[expected.Length];
var total = 0;
while (total < all.Length)
{
var read = stream.Read(all, total, all.Length - total);
Assert.NotEqual(0, read);
total += read;
}

Assert.Equal(expected, all);
}

private static SquashFileSystemReader OpenImage(string name) => new(SquashFixtures.Open(name));

private static string ReadText(SquashFileSystemReader fs, string path)
Expand Down
Loading
Loading