libxfs

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2026 License: MIT Imports: 15 Imported by: 0

README

libxfs

Thread-safe, forensics-friendly XFS parsing in pure Go.

libxfs reads XFS volumes and disk images with strong validation, typed errors, and an API designed for tooling, incident response, and data recovery workflows.

Why libxfs

  • Pure Go parser with zero external runtime dependencies
  • Concurrency-safe volume APIs for parallel reads
  • Corruption-aware parsing with strict bounds checks
  • Typed errors with context (errors.Is / errors.As friendly)
  • Practical path-based and inode-based APIs for common workflows
  • Built-in support for fragmentation analysis and deleted directory artifact scanning
  • Self-checking completeness: reconciles a directory walk against every inode the filesystem has allocated, and surfaces the ones no directory reaches

Installation

go get github.com/aoiflux/libxfs

Go Version

  • Requires Go 1.21+
  • Tested on Go 1.21 through current stable, on Linux, Windows and macOS

Quick Start

package main

import (
	"fmt"
	"log"

	"github.com/aoiflux/libxfs"
)

func main() {
	vol, err := libxfs.OpenVolumeFromPath("disk.img")
	if err != nil {
		log.Fatal(err)
	}
	defer vol.Close()

	sb := vol.Superblock()
	fmt.Printf("XFS v%d block=%d inode=%d root=%d\n",
		sb.FormatVersion,
		sb.BlockSize,
		sb.InodeSize,
		sb.RootDirectoryInodeNumber,
	)

	entries, err := vol.ListRootDirectoryEntries()
	if err != nil {
		log.Fatal(err)
	}
	for _, e := range entries {
		fmt.Printf("%s -> inode %d\n", e.Name, e.InodeNumber)
	}
}

Feature Support

Implemented:

  • Superblock parsing and validation, including the v5 feature words
  • 64-bit "bigtime" inode timestamps and 64-bit (nrext64) extent counters
  • AGI and inode-btree driven inode lookup
  • Inode parsing (v1/v2/v3 layouts)
  • Data reads from inline, extent-list, and extent-btree-backed forks
  • Attribute fork parsing and xattr listing
  • Short-form and block-based xattrs (including remote values)
  • Directory listing for every XFS directory layout:
    • short-form inline directories
    • block-format directories (XD2B / XDB3)
    • leaf, node and btree directories, walked across all data blocks
    • directory blocks larger than the filesystem block size (sb_dirblklog > 0)
  • Directory entry file types (ftype) and parent-inode recovery
  • Path resolution and file reads by absolute path
  • File extraction support via example tooling
  • Fragmentation analysis APIs
  • Directory forensics APIs (active entries, deleted slots, carved deleted candidates)

Current limitations:

  • Directory lookups walk data blocks linearly; the leaf/node hash index is not used to accelerate path resolution on very large directories
  • Deleted entry recovery uses heuristic carving and is explicitly labelled probabilistic output (see Directory Forensics below)
  • Write support is intentionally out of scope (read-only parsing library)

API Highlights

Volume-level:

  • Open(reader io.ReaderAt) (*Volume, error)
  • OpenVolumeFromPath(path string) (*Volume, error)
  • (*Volume).Close() error
  • (*Volume).Superblock() Superblock
  • (*Volume).GetRootInode() (*Inode, error)

Inode/path access:

  • (*Volume).OpenInode(inodeNumber uint64) (*Inode, error)
  • (*Volume).OpenInodeByPath(path string) (*Inode, error)
  • (*Volume).ResolveInodeByPath(path string) (uint64, error)

Directory and file reads:

  • (*Volume).ListDirectoryEntries(inodeNumber uint64) ([]DirectoryEntry, error)
  • (*Volume).ListDirectoryEntriesByPath(path string) ([]DirectoryEntry, error)
  • (*Volume).ListRootDirectoryEntries() ([]DirectoryEntry, error)
  • (*Volume).ListDirectoryEntriesWithOptions(inodeNumber uint64, options DirectoryScanOptions) (DirectoryListing, error)
  • (*Volume).DirectoryParentInode(inodeNumber uint64) (uint64, error)
  • (*Volume).DirectoryParentInodeByPath(path string) (uint64, error)
  • (*Volume).ReadInodeData(inodeNumber uint64, p []byte, off int64) (int, error)
  • (*Volume).ReadFileData(inodeNumber uint64) ([]byte, error)
  • (*Volume).ReadFileDataByPath(path string) ([]byte, error)

xattrs:

  • (*Volume).ListInodeExtendedAttributes(inodeNumber uint64) ([]ExtendedAttribute, error)

Forensics:

  • (*Volume).AnalyzeInodeFragmentation(inodeNumber uint64) (FragmentationReport, error)
  • (*Volume).AnalyzeInodeFragmentationByPath(path string) (FragmentationReport, error)
  • (*Volume).ScanDirectoryRecords(inodeNumber uint64) ([]DirectoryRecord, error)
  • (*Volume).ScanDirectoryRecordsByPath(path string) ([]DirectoryRecord, error)
  • (*Volume).ScanDirectoryRecordsWithOptions(inodeNumber uint64, options DirectoryScanOptions) (DirectoryListing, error)
  • (*Volume).ScanDirectoryRecordsByPathWithOptions(path string, options DirectoryScanOptions) (DirectoryListing, error)
  • (*Volume).VerifyDirectoryIndex(inodeNumber uint64) (DirectoryIndexReport, error)
  • (*Volume).VerifyDirectoryIndexByPath(path string) (DirectoryIndexReport, error)
  • (*Volume).VolumeIntegrityReport() (VolumeIntegrityReport, error)
  • (*Volume).InodeForensicReport(inodeNumber uint64) (InodeForensicReport, error)
  • (*Volume).InodeForensicReportByPath(path string) (InodeForensicReport, error)
  • (*Volume).DirectoryArtifactReport(inodeNumber uint64) (DirectoryArtifactReport, error)
  • (*Volume).DirectoryArtifactReportByPath(path string) (DirectoryArtifactReport, error)
  • (*Volume).Report() (*XFSReport, error)
  • (*Volume).ReportWithOptions(options ReportOptions) (*XFSReport, error)

Completeness and unreachable inodes:

  • (*Volume).EnumerateAllocatedInodes(ctx context.Context, options InodeEnumerationOptions) (InodeEnumeration, error)
  • (*Volume).WalkInodeChunks(ctx context.Context, options InodeEnumerationOptions, fn func(InodeChunk) error) (InodeEnumeration, error)
  • (*Volume).InodeCompletenessReport(ctx context.Context, options InodeCompletenessOptions) (InodeCompletenessReport, error)
  • (*Volume).UnlinkedInodes(ctx context.Context) ([]UnlinkedInode, []ReportAnomaly, error)
  • (*Volume).AllocationGroupCount() int
  • (*Volume).AllocationGroupInodeInfo(index int) (InodeInformation, error)
  • Superblock.AllocatedInodes() uint64, Superblock.MetadataInodeNumbers() []uint64

Completeness

A directory walk answers "what can be reached from the root", which is not the same question as "what does this volume hold". An inode unlinked while a process still held it open, or one whose parent directory entry was lost, is allocated and fully readable but appears in no directory. A tree walk cannot see it, and cannot tell you that it failed to.

EnumerateAllocatedInodes reads the per-allocation-group inode B+trees — the filesystem's own record of which inodes exist — so it returns the complete set regardless of reachability. InodeCompletenessReport reconciles that set against a walk of the tree:

report, err := volume.InodeCompletenessReport(ctx, libxfs.InodeCompletenessOptions{})
if err != nil {
    return err
}

fmt.Print(report.Summary())
for _, orphan := range report.Orphans {
    fmt.Println(orphan.InodeNumber, orphan.Class, orphan.Size)
    data, _ := volume.ReadFileData(orphan.InodeNumber) // still entirely readable
    _ = data
}

Every allocated inode is placed in exactly one of four classes:

Class Meaning
reachable Some path from the root leads to it.
metadata The filesystem allocated it for itself (realtime bitmap/summary, quota files). Unreachable by design.
unlinked On an AGI unlinked chain: deleted while a process still had it open. The filesystem itself records the deletion.
unreferenced Allocated, not metadata, on no chain, and named by no reachable directory. Something was lost.

report.Balanced is the verdict. It is true only when those four add up to what the allocation group headers say the volume holds, which is what lets a file listing be presented as complete rather than merely as what was found. When it is false, report.Anomalies says what did not reconcile, and no completeness claim should be made.

Three points worth knowing:

  • metadata is not a finding. Treating unreachable as synonymous with deleted would report the quota and realtime inodes as recoverable deleted files on every volume that has them.
  • unlinked outranks inference. An inode on an AGI chain was deleted, and the filesystem says so. An unreferenced inode might have been deleted, or might have lost the directory that named it; the two are not conflated.
  • The superblock is not the standard. sb_icount and sb_ifree are lazily maintained on most filesystems and are only written on a clean unmount, so on an image captured from a running system they are stale. They are compared and reported, but disagreement is judged against agi_count/agi_freecount, which are updated transactionally. See Superblock.LazySuperblockCounters().

Reconciliation walks every allocation group's inode B+tree, so Report() omits it unless ReportOptions.IncludeInodeCompleteness asks for it. On a volume with very many inodes, WalkInodeChunks streams instead of materialising the set.

Where an allocation group's inode B+tree cannot be walked at all, InodeEnumerationOptions.BestEffort falls back to a linear scan for inode chunks. That result is labelled chunk_scan, and any report built on it is marked unbalanced: it is evidence, not proof.

Directory Forensics

ScanDirectoryRecords returns active entries, free-space runs and carved candidates together. Confidence alone is not a safe gate: an intact active entry and a strong carve candidate can both be high. Switch on Kind, or use the helpers.

Kind Meaning Safe to present as fact
RecordKindActive Parsed from intact directory framing Yes
RecordKindFreeSlot Reclaimed space; carries no name or inode number Not an entry at all
RecordKindCarved Pattern-matched inside reclaimed space No — candidate only
listing, err := vol.ScanDirectoryRecordsWithOptions(ino, libxfs.DirectoryScanOptions{
    BestEffort: true, // keep what parsed, report anomalies, instead of failing
})

for _, r := range listing.Records {
    switch {
    case r.IsVerified():
        // Fact: an active entry.
    case r.IsProbabilistic():
        // Candidate: may be stale, partial, or a coincidental byte pattern.
        // r.Confidence and r.ConfidenceReasons explain how strong the match is.
    }
}

ConfidenceReasons carries machine-readable evidence codes (for example tag_matches_offset, name_printable, aligned_offset, ftype_valid) so forensic output can show its reasoning rather than assert a verdict.

Every record is addressable within the directory stream via BlockIndex and LogicalOffset; Offset is only meaningful within its own directory block.

Directory index verification

Leaf and node format directories carry a hash index alongside their entries. The kernel maintains both together, so disagreement between them means something modified the directory without maintaining both.

report, err := vol.VerifyDirectoryIndex(ino)
if err == nil && report.HasIndex && !report.Consistent() {
    // report.MissingFromIndex, report.HashMismatches, report.DanglingIndexEntries
}

Directories with no index — short-form and single-block — report HasIndex false and are consistent by definition. Path resolution uses the index when it is present and falls back to a linear walk whenever it is absent, unreadable, or inconsistent; an index never reduces what can be recovered.

Damaged directories

A directory is a sequence of independently framed blocks, so damage to one says nothing about the others. An unreadable or unrecognisable block costs only itself: the scan records an anomaly, continues through the remaining blocks, and returns everything it recovered alongside the error. Discarding the rest would silently remove the whole subtree beneath that directory from a recursive walk.

BestEffort additionally resynchronises within a damaged block, recovering entries either side of the damage instead of stopping at the first bad framing.

Because entries are returned with the error, check both:

listing, err := vol.ListDirectoryEntriesReport(ino)
// listing.Entries is usable even when err != nil.
if errors.Is(err, libxfs.ErrDirectoryTruncated) {
    // A safety cap was reached; the listing is a prefix, not the directory.
}
for _, a := range listing.Anomalies { /* what was skipped, and why */ }

ListDirectoryEntries returns only the entries, so it cannot distinguish a complete listing from a partial one — use ListDirectoryEntriesReport when completeness matters. MaxBlocks and MaxEntries bound the work and set Truncated; when the default caps are what stopped the scan the error is ErrDirectoryTruncated, since a caller who did not ask for a cap has no other way to know one exists.

Deleted entry recovery

Carved candidates come from entry bytes that survive inside reclaimed space. What survives is decided by XFS, not by this library: freeing an entry writes a free-run header over the first four bytes of the record, destroying that entry's inode number, while entries behind it in a coalesced run keep their bytes intact.

So recall is a property of the image, not a guarantee. On the regression corpus, deleting two contiguous runs of 40 entries recovers 78 of the 80 names — each run loses exactly the record whose header was overwritten. Isolated single deletions typically recover nothing, and this is expected rather than a defect.

Soundness is the part that is guaranteed and tested: a deleted name is never reported as a live entry, a live entry is never missing, and a carved record is never presented as fact.

Filesystem Features

v5 filesystems record feature flags that change the on-disk layout. libxfs reads them and adapts; an image carrying an incompatible feature it does not understand is refused rather than silently misparsed.

sb := vol.Superblock()
sb.HasBigTimestamps()     // 64-bit nanosecond timestamps (mkfs default since 2021)
sb.HasLargeExtentCounts() // nrext64: 64-bit extent counters
sb.NeedsRepair()          // filesystem was left needing repair; treat metadata with suspicion

Timestamps matter most here. A bigtime timestamp decoded as the legacy format yields a date roughly 27 years early, which is plausible enough to pass unnoticed, so Inode.HasBigTimestamps records which encoding was used.

Error Handling

libxfs uses wrapped typed errors so callers can reliably inspect failures.

if _, err := vol.ResolveInodeByPath("/nope"); err != nil {
	if errors.Is(err, libxfs.ErrInodeNotFound) {
		// path component not found
	}

	var pErr *libxfs.ParseError
	if errors.As(err, &pErr) {
		fmt.Printf("parse field=%s offset=%d\n", pErr.Field, pErr.Offset)
	}
}

Examples

See examples/README.md for full details.

Available examples:

  • examples/basic: open volume, show metadata, list root directory
  • examples/traverse: recursive traversal with depth control
  • examples/xattrs: list inode extended attributes
  • examples/inode_read: read bytes from inode data stream
  • examples/extract: extract a file by absolute XFS path
  • examples/fragmentation: report file fragmentation and logical holes
  • examples/forensics: inspect active/deleted directory records and carved candidates
  • examples/report: build structured forensic report output (JSON + summary)
  • examples/dirscan: scan a large or damaged directory with best-effort recovery, separating verified entries from carved candidates
  • examples/completeness: reconcile the walk against every allocated inode and list — and optionally extract — the ones no directory reaches

Run one example:

cd examples/basic
go run . <xfs_volume_or_image>

Platform Notes

Raw volume access usually requires elevated privileges.

Windows:

  • Run terminal as Administrator
  • Use paths like \\.\\C: or \\.\\PhysicalDrive0

Linux:

  • Use block-device paths like /dev/sda1
  • Prefer read-only / forensic-safe acquisition workflows

Disk image files are supported on all platforms.

Development

Run checks:

go test ./...
go vet ./...

Conformance tests can be run against a real XFS image. They are skipped unless an image is supplied:

LIBXFS_TEST_IMAGE=/path/to/xfs.dd go test ./...

Among them, TestImageWalkIsComplete reconciles that image's directory walk against its own inode accounting and reports whether the file listing is the whole filesystem — and if not, how many inodes are missing from it and why. It needs no mount, no root and no xfsprogs, so it is the quickest way to put a number on an acquired image:

LIBXFS_TEST_IMAGE=/path/to/image.dd go test -run TestImageWalkIsComplete -v .

Synthetic fixtures only prove the parser agrees with its author's reading of the format. Testing against an image from mkfs.xfs is what catches a misunderstanding shared by both the parser and its fixtures.

Walk completeness

A directory walk that returns plausible results is not the same as one that returns every entry. Establishing completeness needs an oracle that shares no code with this library, so tools/corpus/ builds one.

tools/corpus/mkcorpus.sh creates a corpus of real images spanning every directory format — short-form, block, leaf and node, each with both extent-mapped and b+tree-mapped data forks — plus punched holes, sparse and preallocated files, hardlinks, device nodes, 255-byte and non-ASCII names, 1 KiB/4 KiB/16 KiB geometries, v4 and v4-without-ftype images, eight-allocation- group and sparse/dense inode-chunk images, one image with a directory block deliberately destroyed, one with a recorded set of entries deleted from a live directory, and one snapshotted while three files were deleted but still open.

That last case is the only way to get a populated AGI unlinked list: processing those chains is part of unmounting, so the image is frozen and copied with the file descriptors still held. It is the one image in the corpus containing inodes that hold data and that no directory names at all.

tools/corpus/mkoracle.sh describes each image twice: once through a read-only kernel mount, and once through xfs_db, which needs neither a mount nor kernel XFS support. The two are checked against each other before either is used to judge libxfs. The second oracle is what covers v4 images, which kernels built without CONFIG_XFS_SUPPORT_V4 refuse to mount at all.

The reverse gap exists too, on exactly one case. xfs_db's blockget stops part way through a filesystem it considers damaged, and an inode that no directory references counts as damage, so on the orphan image it names a handful of inodes and gives up. Its name table is recorded as truncated and only the kernel's walk is used for that case; the superblock fields and allocation group headers xfs_db produces come from different code paths and are unaffected, so it still corroborates the inode accounting there.

Both describe what a walk should find. The inode accounting — sb_icount, sb_ifree, and every allocation group's agi_count, agi_freecount and agi_unlinked — answers the different question of how many inodes exist at all, and is what the completeness reconciliation is measured against. Where the kernel offers xfs_io -c bulkstat, that enumeration is captured too: it iterates the inode B+trees inside the kernel and is blind to the directory tree, which makes it the only external oracle answering the same question EnumerateAllocatedInodes does.

Building the corpus needs Linux, root and xfsprogs:

sudo apt install xfsprogs attr
sudo bash tools/corpus/mkcorpus.sh
sudo bash tools/corpus/mkoracle.sh
bash tools/corpus/runtests.sh

The tests compare the walk against the oracle path by path and directory by directory, attribute any shortfall to the on-disk format of the directory that should have produced it, verify each directory's hash index against its own data blocks, and check every file's contents against the kernel's digest.

Committed fixtures

The corpus itself is not committed, but its conclusions are. testdata/corpus/ holds a manifest per case recording what the oracle determined that image contains: total paths, per-directory entry counts, on-disk formats, digests over the exact set of paths, inode numbers, kinds and sizes, and the inode accounting — sb_icount, sb_ifree, the per-allocation-group counts, the filesystem's own reserved inodes, any unlinked chain heads, and how many inodes fall into each of the four completeness classes. They are small, reviewable text, so a change to one is a visible change in what the library is expected to find.

The per-class counts are checked separately from the total on purpose. A regression that moved inodes between classes while leaving the sum correct would reconcile perfectly and still be presenting live files as deleted.

The accounting is what makes the completeness claim portable: the corpus tests establish it on Linux with root and xfsprogs, and these numbers carry the same conclusion to a plain go test anywhere.

The images those manifests describe are not committed — each is a megabyte or more of binary. Rebuild them from the corpus with:

sudo bash tools/corpus/mkfixtures.sh

This writes a metadata-only copy of each image, produced with xfs_metadump, next to its manifest. Once built, go test ./... checks them on any platform with no corpus, no root and no xfsprogs. Cases whose image has not been built are skipped, so the suite passes on a fresh checkout.

The fixtures assert structure — paths, inode numbers, kinds, sizes, per-directory counts and on-disk formats — and never file contents, because xfs_metadump zeroes file data by design. Contents are checked against the kernel by the corpus tests.

Project Status

Core parsing features and forensic helper APIs are implemented and tested, including validation against real mkfs.xfs images, with ongoing hardening around malformed metadata and edge-case directory layouts.

Documentation

Overview

Package libxfs is a read-only, forensics-oriented parser for the XFS filesystem, written in pure Go with no external dependencies.

Entry points

Open a volume from any io.ReaderAt — a file, a device handle, or a section of a larger image — with Open, or from a path with OpenVolumeFromPath:

volume, err := libxfs.OpenVolumeFromPath("disk.img")
if err != nil {
	return err
}
defer volume.Close()

entries, err := volume.ListRootDirectoryEntries()
if err != nil {
	return err
}
for _, entry := range entries {
	fmt.Println(entry.Name, entry.InodeNumber, libxfs.DirEntryFileTypeName(entry.FileType))
}

From a volume, the API divides into five groups:

Completeness, and the inodes a walk cannot reach

Walking directories answers "what can be reached from the root", which is not the same question as "what does this volume hold". An inode that was unlinked while a process still had it open, or whose parent directory entry was lost, is allocated and fully readable but appears in no directory. A tree walk cannot see it, and cannot tell you that it failed to.

Volume.EnumerateAllocatedInodes reads the per-allocation-group inode b-trees, which is the filesystem's own record of which inodes exist, and so returns the complete set regardless of reachability. Volume.InodeCompletenessReport reconciles that set against a walk of the tree and partitions it four ways — reachable, filesystem metadata, unlinked, and unreferenced — checking the total against both the allocation group headers and the superblock counters:

report, err := volume.InodeCompletenessReport(ctx, libxfs.InodeCompletenessOptions{})
if err != nil {
	return err
}
if !report.Balanced {
	// Anomalies says what did not add up; the listing is not complete.
}
for _, orphan := range report.Orphans {
	fmt.Println(orphan.InodeNumber, orphan.Class, orphan.Size)
}

Balanced reports that every inode the filesystem says it has was accounted for, from three sources maintained independently of each other. That is what lets a file listing be presented as complete rather than merely as what was found. Orphans holds the unlinked and unreferenced inodes, which are readable with the ordinary content API.

The reconciliation is not free — it walks every allocation group's inode b-tree — so Volume.Report omits it unless ReportOptions.IncludeInodeCompleteness asks for it.

Reading damaged images

The default behaviour is strict: a malformed structure is reported as an error rather than guessed at. Forensic callers usually want the opposite, so the directory scanners accept DirectoryScanOptions with BestEffort set, which keeps whatever was recovered from healthy blocks, resynchronises past the damage, and records a ReportAnomaly for each problem found.

Work is bounded on hostile input. Sizes are validated against the volume's own capacity, directory walks allocate a single directory block at a time rather than a recorded size, and path resolution is protected against directory loops.

Facts versus candidates

Deleted directory entries are recovered by carving reclaimed space, which is inherently probabilistic. Every DirectoryRecord therefore says how it was obtained: use DirectoryRecord.IsVerified for records parsed from intact framing and DirectoryRecord.IsProbabilistic for carve candidates. Do not gate on Confidence alone — an active entry and a strong carve candidate can both be ConfidenceHigh.

Format coverage

Both v4 and v5 (CRC) filesystems are supported, including short-form, block, leaf, node and btree directories, directory blocks larger than the filesystem block size, extent-list and b-tree data forks, short-form and block-based extended attributes with remote values, 64-bit "bigtime" timestamps, 64-bit (nrext64) extent counters, and both the classic and sparse-inode layouts of the inode b-tree records. An image carrying an incompatible feature this parser does not understand is refused rather than silently misread.

Writing is out of scope; this package never modifies its input.

Index

Constants

View Source
const (
	FileTypeFIFO            uint16 = 0x1000
	FileTypeCharacterDevice uint16 = 0x2000
	FileTypeDirectory       uint16 = 0x4000
	FileTypeBlockDevice     uint16 = 0x6000
	FileTypeRegularFile     uint16 = 0x8000
	FileTypeSymbolicLink    uint16 = 0xa000
	FileTypeSocket          uint16 = 0xc000
)
View Source
const (
	ForkTypeDevice     uint8 = 0
	ForkTypeInlineData uint8 = 1
	ForkTypeExtents    uint8 = 2
	ForkTypeBtree      uint8 = 3
)
View Source
const (
	// FeatureIncompatFileType marks directory entries as carrying an ftype byte.
	FeatureIncompatFileType uint32 = 1 << 0
	// FeatureIncompatSparseInodes marks sparse inode chunk allocation.
	FeatureIncompatSparseInodes uint32 = 1 << 1
	// FeatureIncompatMetaUUID marks metadata stamped with a separate UUID.
	FeatureIncompatMetaUUID uint32 = 1 << 2
	// FeatureIncompatBigTime marks 64-bit nanosecond inode timestamps.
	FeatureIncompatBigTime uint32 = 1 << 3
	// FeatureIncompatNeedsRepair marks a filesystem left needing repair.
	FeatureIncompatNeedsRepair uint32 = 1 << 4
	// FeatureIncompatLargeExtentCounts marks 64-bit inode extent counters
	// (the nrext64 feature).
	FeatureIncompatLargeExtentCounts uint32 = 1 << 5
)

Incompatible feature bits from the v5 superblock (sb_features_incompat).

A filesystem carrying an incompatible bit cannot be interpreted correctly by an implementation that does not understand it. BigTime and LargeExtentCounts both change the on-disk inode layout.

View Source
const (
	// ExtentFlagSparse marks a range that reads back as zeros. It covers both
	// unmapped holes and preallocated-but-unwritten extents, because from a
	// reader's point of view they are the same thing.
	ExtentFlagSparse uint32 = 0x00000001
	// ExtentFlagUnwritten marks a range that is allocated on disk but has
	// never been written, as produced by fallocate. It is always accompanied
	// by ExtentFlagSparse, since it too reads as zeros, but unlike a hole it
	// has a real PhysicalBlockNumber and occupies space.
	//
	// The distinction matters forensically: a hole says nothing was ever
	// stored there, while an unwritten extent names blocks that were reserved,
	// and whose previous contents may still be on the medium.
	ExtentFlagUnwritten uint32 = 0x00000002
)

Extent range flags reported in Extent.RangeFlags.

View Source
const (
	DirEntryFileTypeUnknown         uint8 = 0
	DirEntryFileTypeRegularFile     uint8 = 1
	DirEntryFileTypeDirectory       uint8 = 2
	DirEntryFileTypeCharacterDevice uint8 = 3
	DirEntryFileTypeBlockDevice     uint8 = 4
	DirEntryFileTypeFIFO            uint8 = 5
	DirEntryFileTypeSocket          uint8 = 6
	DirEntryFileTypeSymbolicLink    uint8 = 7
	DirEntryFileTypeWhiteout        uint8 = 8
)

Directory entry file types (XFS_DIR3_FT_*).

These are stored in the optional ftype byte of a directory entry when the filesystem has the ftype feature enabled, and describe the target inode without requiring it to be read.

View Source
const (
	DirectoryFormatShortForm  = "short_form"
	DirectoryFormatBlock      = "block"
	DirectoryFormatMultiBlock = "multi_block"
)

Directory layout names reported in DirectoryListing.Format.

View Source
const (
	DirectorySourceFormatShortForm = "short_form"
	DirectorySourceFormatBlock     = "block"
	DirectorySourceFormatLeaf      = "leaf"
	DirectorySourceFormatNode      = "node"
)

On-disk directory index formats reported in DirectoryListing.SourceFormat.

View Source
const (
	XattrNamespaceUser     = "user"
	XattrNamespaceTrusted  = "trusted"
	XattrNamespaceSecurity = "security"
)

Extended attribute namespace names, as Linux presents them. Downstream tools match on these strings, so they are part of the library's contract.

View Source
const (
	SeverityInfo    = "info"
	SeverityLow     = "low"
	SeverityMedium  = "medium"
	SeverityWarning = "warning"
	SeverityHigh    = "high"
	SeverityError   = "error"
)

Severity levels reported in ReportAnomaly.Severity.

View Source
const (
	InodeTypeFile      = "file"
	InodeTypeDirectory = "directory"
)

Inode type labels reported in InodeForensicReport.Type.

View Source
const (
	ReasonIntactFraming    = "intact_framing"
	ReasonTagMatchesOffset = "tag_matches_offset"
	ReasonNamePrintable    = "name_printable"
	ReasonAlignedOffset    = "aligned_offset"
	ReasonFileTypeValid    = "ftype_valid"
	ReasonInodeAllocated   = "inode_allocated"
	ReasonInodeUnallocated = "inode_unallocated"
	// ReasonInodeUnaddressable marks a recovered inode number that cannot
	// address anything on this volume — strong evidence of a false match.
	ReasonInodeUnaddressable = "inode_unaddressable"
	ReasonInFreeSlot         = "in_free_slot"
)

Evidence codes reported in DirectoryRecord.ConfidenceReasons.

View Source
const FilesystemTypeName = "xfs"

FilesystemTypeName is the filesystem label reported by VolumeIntegrityReport.

Variables

View Source
var (
	ErrInvalidSuperblock      = errors.New("invalid or corrupted XFS superblock")
	ErrInvalidInode           = errors.New("invalid or corrupted XFS inode")
	ErrInvalidInodeNumber     = errors.New("invalid inode number")
	ErrInvalidPath            = errors.New("invalid path")
	ErrInvalidInodeInfo       = errors.New("invalid allocation-group inode information")
	ErrInvalidAttributeData   = errors.New("invalid attribute fork data")
	ErrUnsupportedDirFormat   = errors.New("unsupported directory format")
	ErrInodeNotFound          = errors.New("inode not found")
	ErrUnsupportedFeatureFlag = errors.New("unsupported XFS feature flag")
	ErrUnsupportedXattrFormat = errors.New("unsupported extended attribute format")
	ErrVolumeClosed           = errors.New("volume is closed")
	ErrVerificationFailed     = errors.New("forensic verification failed")
	// ErrDirectoryTruncated reports that a directory scan stopped at a
	// configured cap and the listing is therefore not the whole directory.
	// It is returned alongside everything that was recovered.
	ErrDirectoryTruncated = errors.New("directory scan truncated at a configured limit")
)

Functions

func DirEntryFileTypeName

func DirEntryFileTypeName(fileType uint8) string

DirEntryFileTypeName returns a human readable name for an XFS directory entry file type value.

Types

type AllocatedInodeSource added in v0.3.1

type AllocatedInodeSource = string

AllocatedInodeSource labels how a set of allocated inodes was determined.

const (
	// InodeSourceInodeBtree is the allocation group's inode b-tree: the
	// filesystem's own authoritative record of which inodes exist. Treat its
	// output as fact.
	InodeSourceInodeBtree AllocatedInodeSource = "inode_btree"
	// InodeSourceChunkScan is a linear sweep of the allocation group looking
	// for inode chunk magics, used only where the b-tree could not be walked.
	// It is best effort: it can miss chunks whose first inode is damaged, and
	// it can report stale inodes left in reclaimed space.
	InodeSourceChunkScan AllocatedInodeSource = "chunk_scan"
	// InodeSourceMixed means some allocation groups were read from the b-tree
	// and others from a fallback scan. Per-group sources are in
	// InodeEnumeration.PerAllocationGroup.
	InodeSourceMixed AllocatedInodeSource = "mixed"
)

type AllocationGroupInodeCounts added in v0.3.1

type AllocationGroupInodeCounts struct {
	AllocationGroup uint32               `json:"allocation_group"`
	Source          AllocatedInodeSource `json:"source"`

	// Chunks, BackedSlots, Allocated and Free are derived by walking.
	Chunks      int    `json:"chunks"`
	BackedSlots uint64 `json:"backed_slots"`
	Allocated   uint64 `json:"allocated"`
	Free        uint64 `json:"free"`

	// AGICount and AGIFreeCount are what agi_count and agi_freecount say. They
	// are maintained transactionally by the kernel and are the standard this
	// walk is measured against.
	AGICount     uint32 `json:"agi_count"`
	AGIFreeCount uint32 `json:"agi_free_count"`

	// Balanced records whether the walk and the AGI agree on both numbers.
	Balanced bool `json:"balanced"`
}

AllocationGroupInodeCounts is what one allocation group was found to hold, beside what its AGI header claims it holds.

type Concurrency

type Concurrency struct {
	// Workers is the maximum number of tasks executed in parallel. Zero or one
	// runs sequentially on the calling goroutine. Negative means "one per
	// available CPU".
	Workers int
}

Concurrency configures optional parallel execution.

The zero value is sequential.

type DirectoryArtifactReport

type DirectoryArtifactReport struct {
	InodeNumber  uint64            `json:"inode_number"`
	Path         string            `json:"path,omitempty"`
	RecordCount  int               `json:"record_count"`
	ActiveCount  int               `json:"active_count"`
	DeletedCount int               `json:"deleted_count"`
	CarvedCount  int               `json:"carved_count"`
	Records      []DirectoryRecord `json:"records"`
	Anomalies    []ReportAnomaly   `json:"anomalies,omitempty"`
}

DirectoryArtifactReport summarizes active/deleted/carved records for one directory inode.

type DirectoryEntry

type DirectoryEntry struct {
	Name        string
	InodeNumber uint64
	// FileType is the XFS directory entry file type (DirEntryFileType*).
	// It is DirEntryFileTypeUnknown on filesystems without the ftype feature.
	FileType uint8
}

type DirectoryIndexReport

type DirectoryIndexReport struct {
	InodeNumber uint64 `json:"inode_number"`
	// HasIndex is false for short-form and single-block directories, which
	// have no separate hash index to check.
	HasIndex bool `json:"has_index"`
	// IndexedEntries counts usable entries in the hash index.
	IndexedEntries int `json:"indexed_entries"`
	// DataEntries counts active entries found by walking the data blocks.
	DataEntries int `json:"data_entries"`
	// MissingFromIndex lists entries present in the data blocks whose hash and
	// address are absent from the index.
	MissingFromIndex []string `json:"missing_from_index,omitempty"`
	// DanglingIndexEntries counts index entries that do not resolve to a
	// readable directory entry.
	DanglingIndexEntries int `json:"dangling_index_entries"`
	// HashMismatches lists entries whose name does not hash to the value the
	// index records for it.
	HashMismatches []string        `json:"hash_mismatches,omitempty"`
	Anomalies      []ReportAnomaly `json:"anomalies,omitempty"`
}

DirectoryIndexReport compares a directory's hash index against the entries actually present in its data blocks.

The two structures are maintained together by the kernel, so any divergence means the directory was modified by something that did not maintain both — a tampering indicator that no other check in this package provides.

func (DirectoryIndexReport) Consistent

func (r DirectoryIndexReport) Consistent() bool

Consistent reports whether the index and the data blocks agree.

type DirectoryListing

type DirectoryListing struct {
	InodeNumber uint64 `json:"inode_number"`
	// Entries holds active entries only, in on-disk order.
	Entries []DirectoryEntry `json:"entries,omitempty"`
	// Records holds every record produced by a forensic scan: active entries,
	// free slots and carved candidates. It is populated by the
	// ScanDirectoryRecords* APIs. A plain listing leaves it empty, since
	// building it would double the cost of the common path; use Entries there.
	Records []DirectoryRecord `json:"records,omitempty"`
	// Anomalies records structural problems encountered in best-effort mode.
	Anomalies []ReportAnomaly `json:"anomalies,omitempty"`
	// Truncated reports that a cap was reached and results are incomplete.
	Truncated bool `json:"truncated,omitempty"`
	// BlocksScanned counts directory blocks actually read.
	BlocksScanned uint64 `json:"blocks_scanned,omitempty"`
	// Format names the directory layout that was parsed. It distinguishes only
	// what the walker had to do: a single block, or several. Use SourceFormat
	// to learn the directory's actual on-disk shape.
	Format string `json:"format,omitempty"`
	// SourceFormat names the directory's on-disk index format: short_form,
	// block, leaf or node.
	//
	// It is derived from the inode's fork type and from where the directory's
	// blocks sit in its logical space, not from how many data blocks it has.
	// Two directories with the same data-block count can be in different
	// formats, so Format cannot answer this and must not be used to.
	SourceFormat string `json:"source_format,omitempty"`
}

DirectoryListing is the result of a directory scan.

type DirectoryRecord

type DirectoryRecord struct {
	Name         string
	InodeNumber  uint64
	IsDeleted    bool
	Offset       uint16
	RecordLength uint16
	IsCarved     bool
	Confidence   RecoveryConfidence

	// Kind describes how this record was obtained. Prefer it over the
	// IsDeleted/IsCarved pair when gating downstream decisions.
	Kind DirectoryRecordKind
	// FileType is the XFS directory entry file type (DirEntryFileType*).
	FileType uint8
	// BlockIndex is the directory-block index this record was found in.
	BlockIndex uint64
	// LogicalOffset is the absolute byte offset of the record within the
	// directory data stream. Offset is only meaningful within its block.
	LogicalOffset uint64
	// ConfidenceReasons lists the evidence codes behind Confidence.
	ConfidenceReasons []string
}

DirectoryRecord represents an active or deleted slot recovered from directory data structures.

func (DirectoryRecord) IsProbabilistic

func (r DirectoryRecord) IsProbabilistic() bool

IsProbabilistic reports whether the record was carved heuristically and must be presented as a candidate rather than as fact.

func (DirectoryRecord) IsVerified

func (r DirectoryRecord) IsVerified() bool

IsVerified reports whether the record was parsed from intact directory framing and can be treated as fact.

type DirectoryRecordKind

type DirectoryRecordKind = string

DirectoryRecordKind distinguishes how a record was obtained. Confidence alone is not a safe gate: an active entry and a carved candidate can both be ConfidenceHigh. Switch on Kind, or use IsVerified/IsProbabilistic.

const (
	// RecordKindActive is an entry parsed from intact directory framing.
	RecordKindActive DirectoryRecordKind = "active"
	// RecordKindFreeSlot is an unused-space run. It marks reclaimed space and
	// carries no recovered name or inode number.
	RecordKindFreeSlot DirectoryRecordKind = "free_slot"
	// RecordKindCarved is a probabilistic candidate recovered from free space
	// by pattern matching. It may be stale, partial, or entirely spurious.
	RecordKindCarved DirectoryRecordKind = "carved"
)

type DirectoryScanOptions

type DirectoryScanOptions struct {
	// IncludeDeleted reports free-space runs and carved candidates alongside
	// active entries.
	IncludeDeleted bool
	// BestEffort keeps whatever was recovered when a block is malformed,
	// recording a ReportAnomaly and resynchronising instead of failing. This
	// is usually what forensic callers want on a damaged image.
	BestEffort bool
	// MaxBlocks caps the number of directory blocks read. Zero applies the
	// default cap.
	MaxBlocks uint64
	// MaxEntries caps the number of records collected. Zero applies the
	// default cap.
	MaxEntries int
	// contains filtered or unexported fields
}

DirectoryScanOptions controls how a directory is walked.

The zero value is strict: any framing error aborts the scan, and only active entries are reported.

type ExtendedAttribute

type ExtendedAttribute struct {
	Name      string
	Namespace string
	Value     []byte
	Flags     uint8
}

type Extent

type Extent struct {
	LogicalBlockNumber  uint64
	PhysicalBlockNumber uint64
	NumberOfBlocks      uint32
	RangeFlags          uint32
}

type FragmentationReport

type FragmentationReport struct {
	InodeNumber                uint64
	Size                       uint64
	DataExtentCount            int
	AllocatedExtentCount       int
	SparseExtentCount          int
	PhysicalFragmentRuns       int
	HasLogicalHoles            bool
	HasPhysicalFragmentation   bool
	HasAnyFragmentationOrHoles bool
}

FragmentationReport summarizes how a file's data is laid out across extents.

type IOError

type IOError struct {
	Op     string
	Offset int64
	Size   int
	Err    error
}

func (*IOError) Error

func (e *IOError) Error() string

func (*IOError) Unwrap

func (e *IOError) Unwrap() error

type Inode

type Inode struct {
	FormatVersion uint8
	FileMode      uint16
	ForkType      uint8

	OwnerID       uint32
	GroupID       uint32
	NumberOfLinks uint32

	AccessTimeNS       int64
	ModificationTimeNS int64
	InodeChangeTimeNS  int64
	CreationTimeNS     int64

	Size uint64
	// NumberOfDataExtents is the data fork extent count, saturated to 32 bits.
	// On filesystems with the nrext64 feature the on-disk counter is 64 bits
	// wide; prefer DataExtentCount, which cannot overflow.
	NumberOfDataExtents uint32
	// NumberOfAttributesExtent is the attribute fork extent count, saturated
	// to 16 bits. Prefer AttributeExtentCount.
	NumberOfAttributesExtent uint16
	// DataExtentCount is the full-width data fork extent count.
	DataExtentCount uint64
	// AttributeExtentCount is the full-width attribute fork extent count.
	AttributeExtentCount uint32
	// HasBigTimestamps records whether this inode's timestamps were decoded
	// using the 64-bit bigtime encoding.
	HasBigTimestamps   bool
	AttributesForkType uint8
	DeviceIdentifier   uint32

	DataForkOffset       uint16
	DataForkSize         uint16
	AttributesForkOffset uint16
	AttributesForkSize   uint16

	InlineData  []byte
	DataExtents []Extent

	InlineAttributesData []byte
	AttributesExtents    []Extent
	Raw                  []byte
}

func (*Inode) AccessTime

func (i *Inode) AccessTime() time.Time

func (*Inode) CreationTime

func (i *Inode) CreationTime() time.Time

func (*Inode) InodeChangeTime

func (i *Inode) InodeChangeTime() time.Time

func (*Inode) IsDirectory

func (i *Inode) IsDirectory() bool

func (*Inode) ModificationTime

func (i *Inode) ModificationTime() time.Time

type InodeChunk added in v0.3.1

type InodeChunk struct {
	// AllocationGroup is the group this record was read from.
	AllocationGroup uint32
	// StartInode is the absolute inode number of slot 0. RelativeStartInode is
	// ir_startino, the same value relative to the allocation group.
	StartInode         uint64
	RelativeStartInode uint32

	// HoleMask is ir_holemask. Each of its 16 bits covers four consecutive
	// slots, and a set bit means those four are not backed by disk. It is zero
	// on filesystems without the sparse-inodes feature.
	HoleMask uint16
	// Count is ir_count, the number of slots that are backed by disk. It is 64
	// unless the chunk is sparse.
	Count uint8
	// FreeCount is ir_freecount, how many backed slots are unused.
	FreeCount uint32
	// FreeMask is ir_free. A set bit means that slot is free; note the
	// inversion, which is the opposite of what "free mask" suggests to most
	// readers.
	FreeMask uint64
}

InodeChunk is one inode b-tree record: a run of 64 consecutive inode slots and their allocation state.

Slots are addressed 0..63 relative to StartInode. A slot is one of three things, and the distinction matters: allocated (an inode exists and is in use), free (backed by disk, currently unused, and its previous contents may still be readable), or a hole (no disk behind it at all, possible only with the sparse-inodes feature).

func (InodeChunk) AllocatedCount added in v0.3.1

func (c InodeChunk) AllocatedCount() int

AllocatedCount returns how many slots in this chunk hold a live inode.

func (InodeChunk) AllocatedInodes added in v0.3.1

func (c InodeChunk) AllocatedInodes(dst []uint64) []uint64

AllocatedInodes appends the absolute inode number of every allocated slot to dst, in ascending order, and returns the extended slice.

Appending to a caller-owned slice keeps enumeration of a large filesystem from allocating once per chunk.

func (InodeChunk) InodeNumber added in v0.3.1

func (c InodeChunk) InodeNumber(slot int) uint64

InodeNumber returns the absolute inode number of a slot, without regard to whether it is allocated.

func (InodeChunk) IsAllocated added in v0.3.1

func (c InodeChunk) IsAllocated(slot int) bool

IsAllocated reports whether a slot holds a live inode.

func (InodeChunk) IsFree added in v0.3.1

func (c InodeChunk) IsFree(slot int) bool

IsFree reports whether a slot is backed by disk but holds no live inode.

A free slot is not empty space: unless the chunk was only just allocated, it holds the remains of whatever inode used to live there.

func (InodeChunk) IsHole added in v0.3.1

func (c InodeChunk) IsHole(slot int) bool

IsHole reports whether a slot has no backing disk.

type InodeClass added in v0.3.1

type InodeClass = string

InodeClass says why an allocated inode is where it is.

const (
	// InodeClassReachable means some path from the root leads to this inode.
	InodeClassReachable InodeClass = "reachable"
	// InodeClassMetadata means the filesystem allocated this inode for its own
	// use — a realtime bitmap or summary, or a quota file. It is unreachable by
	// design and is not evidence of anything.
	InodeClassMetadata InodeClass = "metadata"
	// InodeClassUnlinked means the inode is on an AGI unlinked chain: it was
	// deleted while a process still held it open. The filesystem itself is
	// recording the deletion, so this is a finding rather than an inference.
	InodeClassUnlinked InodeClass = "unlinked"
	// InodeClassUnreferenced means the inode is allocated, is not filesystem
	// metadata, is not on an unlinked chain, and no directory reachable from
	// the root names it. Something was lost: either the directory entry that
	// referred to it, or the directory that held that entry.
	InodeClassUnreferenced InodeClass = "unreferenced"
)

type InodeCompletenessOptions added in v0.3.1

type InodeCompletenessOptions struct {
	// RootPath is where the directory walk starts. Defaults to "/". Starting
	// anywhere else makes the balance meaningless, since inodes outside that
	// subtree are unreachable by construction; the report says so.
	RootPath string
	// Enumeration is passed through to [Volume.WalkInodeChunks].
	Enumeration InodeEnumerationOptions
	// MaxWalkEntries caps the directory walk. A capped walk cannot support any
	// conclusion about completeness, so the report is marked unbalanced.
	MaxWalkEntries int
	// MaxOrphans caps how many unreachable inodes are described in detail.
	// Counts are unaffected. Zero means unlimited.
	MaxOrphans int
	// SkipOrphanDetails counts unreachable inodes without opening them. It
	// turns the report into a cheap consistency check on a volume with a very
	// large number of them.
	SkipOrphanDetails bool
	// SkipOrphanedDirectoryDescent stops the report walking into orphaned
	// directories to name what they contain.
	SkipOrphanedDirectoryDescent bool
}

InodeCompletenessOptions controls the reconciliation.

type InodeCompletenessReport added in v0.3.1

type InodeCompletenessReport struct {
	GeneratedAt time.Time            `json:"generated_at"`
	RootPath    string               `json:"root_path"`
	Source      AllocatedInodeSource `json:"source"`

	// The superblock's own accounting, and whether it is the lazily maintained
	// kind that a crash leaves stale.
	SuperblockIcount    uint64 `json:"superblock_icount"`
	SuperblockIfree     uint64 `json:"superblock_ifree"`
	SuperblockAllocated uint64 `json:"superblock_allocated"`
	SuperblockLazy      bool   `json:"superblock_counters_lazy"`

	// The sum of the per-allocation-group headers, which is the standard the
	// walk is judged against.
	AllocationGroupCount     uint64 `json:"allocation_group_count"`
	AllocationGroupFree      uint64 `json:"allocation_group_free"`
	AllocationGroupAllocated uint64 `json:"allocation_group_allocated"`

	// EnumeratedAllocated is what walking the inode b-trees actually found.
	EnumeratedAllocated uint64 `json:"enumerated_allocated"`

	// The four-way partition. Together they must account for every enumerated
	// inode.
	ReachableInodes    uint64 `json:"reachable_inodes"`
	MetadataInodes     uint64 `json:"metadata_inodes"`
	UnlinkedInodes     uint64 `json:"unlinked_inodes"`
	UnreferencedInodes uint64 `json:"unreferenced_inodes"`
	ClassifiedInodes   uint64 `json:"classified_inodes"`

	// Orphans describes the unlinked and unreferenced inodes: everything that
	// exists and holds data but that no directory walk would ever produce.
	Orphans          []RecoverableInode `json:"orphans,omitempty"`
	OrphansTruncated bool               `json:"orphans_truncated"`

	PerAllocationGroup []AllocationGroupInodeCounts `json:"per_allocation_group,omitempty"`

	// Balanced is the conclusion. When it is true, every inode the filesystem
	// says it has was accounted for, from three independently maintained
	// sources, and the file listing is complete. When it is false, Anomalies
	// says what did not add up, and no completeness claim should be made.
	Balanced  bool            `json:"balanced"`
	Anomalies []ReportAnomaly `json:"anomalies,omitempty"`
}

InodeCompletenessReport reconciles a directory walk against everything the filesystem says it holds.

func (*InodeCompletenessReport) Summary added in v0.3.1

func (r *InodeCompletenessReport) Summary() string

Summary renders the reconciliation as a short human-readable block.

type InodeEnumeration added in v0.3.1

type InodeEnumeration struct {
	// Source is InodeSourceInodeBtree when every allocation group was read
	// from its b-tree. Anything else means part of the answer is best effort
	// and must be presented as such.
	Source AllocatedInodeSource `json:"source"`

	// Inodes holds every allocated inode number in ascending order. It is
	// populated by EnumerateAllocatedInodes and left empty by WalkInodeChunks,
	// which streams instead.
	Inodes []uint64 `json:"inodes,omitempty"`
	// Chunks holds the decoded records, when IncludeChunks asked for them.
	Chunks []InodeChunk `json:"chunks,omitempty"`

	PerAllocationGroup []AllocationGroupInodeCounts `json:"per_allocation_group"`

	// AllocatedCount, FreeCount and BackedSlotCount total the per-group
	// figures. AllocatedCount is the number to compare against the superblock's
	// AllocatedInodes.
	AllocatedCount  uint64 `json:"allocated_count"`
	FreeCount       uint64 `json:"free_count"`
	BackedSlotCount uint64 `json:"backed_slot_count"`

	// Truncated reports that MaxInodes cut the collected set short. Counts
	// remain complete; Inodes does not.
	Truncated bool `json:"truncated"`

	// Balanced reports that every allocation group's walk agreed with its AGI
	// header, and that no group needed a fallback scan. When it is false, the
	// enumeration is evidence rather than proof, and Anomalies says why.
	Balanced bool `json:"balanced"`

	Anomalies []ReportAnomaly `json:"anomalies,omitempty"`
}

InodeEnumeration is the result of enumerating allocated inodes.

type InodeEnumerationOptions added in v0.3.1

type InodeEnumerationOptions struct {
	// BestEffort keeps going when an allocation group's inode b-tree cannot be
	// walked: the group falls back to a linear chunk scan and an anomaly is
	// recorded. The zero value fails the whole enumeration instead, which is
	// the right default for a caller that intends to reconcile counts.
	BestEffort bool
	// IncludeChunks retains every decoded chunk record in the result. Off by
	// default because a large filesystem has a great many of them.
	IncludeChunks bool
	// MaxInodes caps how many inode numbers are collected, setting Truncated
	// when it bites. Zero is unlimited. It bounds memory, not work: the walk
	// still visits every group so that the counts stay meaningful.
	MaxInodes int
	// VerifyFreeInodeBtree additionally walks the free inode b-tree and checks
	// that it agrees with the inode b-tree about which inodes are free. The two
	// trees are maintained separately, so a disagreement is a strong signal of
	// damage or tampering. v5 filesystems with the finobt feature only.
	VerifyFreeInodeBtree bool
}

InodeEnumerationOptions controls how the allocated inode set is gathered.

type InodeForensicReport

type InodeForensicReport struct {
	InodeNumber            uint64              `json:"inode_number"`
	Path                   string              `json:"path,omitempty"`
	Type                   string              `json:"type"`
	FileMode               uint16              `json:"file_mode"`
	ForkType               uint8               `json:"fork_type"`
	Size                   uint64              `json:"size"`
	OwnerID                uint32              `json:"owner_id"`
	GroupID                uint32              `json:"group_id"`
	NumberOfLinks          uint32              `json:"number_of_links"`
	AccessTime             time.Time           `json:"access_time"`
	ModificationTime       time.Time           `json:"modification_time"`
	InodeChangeTime        time.Time           `json:"inode_change_time"`
	CreationTime           time.Time           `json:"creation_time,omitempty"`
	DataExtentCount        int                 `json:"data_extent_count"`
	AttributesExtentCount  int                 `json:"attributes_extent_count"`
	HasInlineData          bool                `json:"has_inline_data"`
	Fragmentation          FragmentationReport `json:"fragmentation"`
	Fragments              []InodeFragment     `json:"fragments,omitempty"`
	ExtendedAttributeNames []string            `json:"extended_attribute_names,omitempty"`
	Anomalies              []ReportAnomaly     `json:"anomalies,omitempty"`
}

InodeForensicReport is structured metadata for one inode.

type InodeFragment

type InodeFragment struct {
	StartOffset         uint64 `json:"start_offset"`
	EndOffset           uint64 `json:"end_offset"`
	LengthBytes         uint64 `json:"length_bytes"`
	LogicalBlockNumber  uint64 `json:"logical_block_number"`
	PhysicalBlockNumber uint64 `json:"physical_block_number"`
	NumberOfBlocks      uint32 `json:"number_of_blocks"`
	// IsSparse marks a fragment that reads back as zeros, whether because it
	// is an unmapped hole or because it was preallocated and never written.
	IsSparse bool `json:"is_sparse"`
	// IsUnwritten distinguishes the second case: blocks were reserved on disk
	// and never written to. Unlike a hole, PhysicalBlockNumber names real
	// blocks whose prior contents may still be recoverable from the medium.
	IsUnwritten bool `json:"is_unwritten"`
}

InodeFragment describes one extent from the inode data fork.

type InodeInformation

type InodeInformation struct {
	FormatVersion       uint32
	InodeBtreeRootBlock uint32
	InodeBtreeDepth     uint32
	LastAllocatedChunk  uint32

	// SequenceNumber is agi_seqno, this group's own index. Length is
	// agi_length, its size in filesystem blocks.
	SequenceNumber uint32
	Length         uint32

	// Count is agi_count, the number of inode slots allocated in this group,
	// and FreeCount is agi_freecount, how many of them are unused.
	Count     uint32
	FreeCount uint32

	// LastDirectoryChunk is agi_dirino.
	LastDirectoryChunk uint32

	// UnlinkedBuckets is agi_unlinked: 64 chain heads, each a relative inode
	// number or agiNullInode for an empty bucket. A non-empty bucket names an
	// inode that was unlinked while a process still had it open, which is
	// direct on-disk evidence of a deleted-but-recoverable file. The chains are
	// threaded through di_next_unlinked in the inodes themselves.
	//
	// A cleanly unmounted filesystem has no unlinked inodes left: the kernel
	// processes the chains as part of unmounting. A populated list is therefore
	// a sign the image was captured from a live or crashed system.
	UnlinkedBuckets [agiUnlinkedBuckets]uint32

	// FreeInodeBtreeRootBlock and FreeInodeBtreeDepth locate the free inode
	// b-tree (finobt). Both are zero on v4 filesystems and on v5 filesystems
	// without the finobt feature.
	FreeInodeBtreeRootBlock uint32
	FreeInodeBtreeDepth     uint32

	// InodeBtreeBlocks and FreeInodeBtreeBlocks are agi_iblocks and
	// agi_fblocks, the block counts of the two trees. v5 only.
	InodeBtreeBlocks     uint32
	FreeInodeBtreeBlocks uint32
}

InodeInformation is one allocation group's inode header (xfs_agi).

Unlike the superblock counters, Count and FreeCount are updated transactionally with every inode allocation, so they are the authoritative answer to how many inodes an allocation group holds.

func (InodeInformation) AllocatedInodes added in v0.3.1

func (i InodeInformation) AllocatedInodes() uint32

AllocatedInodes returns the number of inodes in use in this allocation group.

func (InodeInformation) HasFreeInodeBtree added in v0.3.1

func (i InodeInformation) HasFreeInodeBtree() bool

HasFreeInodeBtree reports whether this allocation group carries a finobt that can be walked as an independent check on the inode b-tree.

func (InodeInformation) HasUnlinkedInodes added in v0.3.1

func (i InodeInformation) HasUnlinkedInodes() bool

HasUnlinkedInodes reports whether any unlinked chain in this allocation group is non-empty.

type ParseError

type ParseError struct {
	Offset int64
	Field  string
	Err    error
}

func (*ParseError) Error

func (e *ParseError) Error() string

func (*ParseError) Unwrap

func (e *ParseError) Unwrap() error

type RecoverableInode added in v0.3.1

type RecoverableInode struct {
	InodeNumber     uint64     `json:"inode_number"`
	Class           InodeClass `json:"class"`
	AllocationGroup uint32     `json:"allocation_group"`

	// Type, FileMode, Size and the rest come from the inode itself and are
	// zero when it could not be opened, in which case Anomalies says why. An
	// inode that cannot be opened is still reported: that it exists is a fact
	// established by the inode b-tree, independent of whether it parses.
	Type             string    `json:"type,omitempty"`
	FileMode         uint16    `json:"file_mode,omitempty"`
	Size             uint64    `json:"size,omitempty"`
	OwnerID          uint32    `json:"owner_id,omitempty"`
	GroupID          uint32    `json:"group_id,omitempty"`
	NumberOfLinks    uint32    `json:"number_of_links"`
	ModificationTime time.Time `json:"modification_time,omitempty"`
	InodeChangeTime  time.Time `json:"inode_change_time,omitempty"`

	// UnlinkedBucket is which agi_unlinked chain this inode is on, or -1 when
	// it is on none.
	UnlinkedBucket int `json:"unlinked_bucket"`

	// ParentInodeNumber is what this inode's ".." points at, for directories.
	// ParentPath is that parent's path when the parent is reachable from the
	// root, which locates an orphaned directory in the tree even though the
	// entry naming it is gone.
	ParentInodeNumber uint64 `json:"parent_inode_number,omitempty"`
	ParentPath        string `json:"parent_path,omitempty"`

	// RecoveredPath is set for an inode found by descending an orphaned
	// directory. It is relative to RecoveredPathRoot, the outermost orphaned
	// directory it was reached from, because there is by definition no path
	// from the filesystem root to it.
	RecoveredPath     string `json:"recovered_path,omitempty"`
	RecoveredPathRoot uint64 `json:"recovered_path_root,omitempty"`

	Anomalies []ReportAnomaly `json:"anomalies,omitempty"`
}

RecoverableInode is one allocated inode that the directory walk did not reach, described well enough to act on.

type RecoveryConfidence

type RecoveryConfidence = string

RecoveryConfidence labels how much trust a recovered directory record deserves. It is a string alias so that existing comparisons against plain string literals keep compiling.

const (
	ConfidenceLow    RecoveryConfidence = "low"
	ConfidenceMedium RecoveryConfidence = "medium"
	ConfidenceHigh   RecoveryConfidence = "high"
)

Confidence levels applied to DirectoryRecord.Confidence.

type ReportAnomaly

type ReportAnomaly struct {
	Code     string `json:"code"`
	Severity string `json:"severity"`
	Message  string `json:"message"`
	Path     string `json:"path,omitempty"`
	Inode    uint64 `json:"inode,omitempty"`
}

ReportAnomaly captures a parsing or consistency concern encountered while building reports.

type ReportOptions

type ReportOptions struct {
	// RootPath selects the start path for traversal. Defaults to "/".
	RootPath string
	// MaxEntries limits the number of discovered inodes in Files.
	// Zero or negative means unlimited.
	MaxEntries int
	// IncludeDirectoryArtifacts includes deleted/carved directory record output
	// for each visited directory inode.
	IncludeDirectoryArtifacts bool
	// VerificationMode controls whether checksum/verification mismatches are
	// fatal (`strict`) or recorded as anomalies (`best_effort`).
	VerificationMode VerificationMode
	// IncludeInodeCompleteness reconciles the walk against every inode the
	// filesystem has allocated, and reports the ones no directory reaches.
	//
	// It is off by default because it walks every allocation group's inode
	// b-tree, which a caller who only wants a file listing should not pay for.
	// Turn it on to be able to say the listing is complete, and to surface
	// deleted-but-open and orphaned files, which no tree walk can find.
	IncludeInodeCompleteness bool
	// InodeCompleteness tunes that reconciliation. Its RootPath is ignored in
	// favour of ReportOptions.RootPath, and its MaxWalkEntries in favour of
	// MaxEntries, so that both walks describe the same thing.
	InodeCompleteness InodeCompletenessOptions
	// Concurrency optionally analyses discovered inodes in parallel. The
	// zero value is sequential. Output is identical regardless of the
	// worker count.
	Concurrency Concurrency
}

ReportOptions controls how volume-level report generation behaves.

type ReportProvenance

type ReportProvenance struct {
	VerificationMode     VerificationMode `json:"verification_mode"`
	Coverage             []string         `json:"coverage"`
	SuperblockCRCChecked bool             `json:"superblock_crc_checked"`
	InodeCRCChecked      bool             `json:"inode_crc_checked"`
}

ReportProvenance captures parser and verification context for reproducibility.

type Superblock

type Superblock struct {
	BlockSize                uint32
	NumberOfBlocks           uint64
	JournalBlockNumber       uint64
	RootDirectoryInodeNumber uint64
	AllocationGroupSize      uint32
	NumberOfAllocationGroups uint32
	FormatVersion            uint8
	FeatureFlags             uint16
	SectorSize               uint16
	InodeSize                uint16
	DirectoryBlockSize       uint32
	VolumeLabel              [12]byte
	SecondaryFeatureFlags    uint32
	RelativeBlockNumberBits  uint8
	RelativeInodeNumberBits  uint8

	// v5-only feature words. These are zero on v4 filesystems, which do not
	// have the fields at all.
	FeaturesCompat         uint32
	FeaturesReadOnlyCompat uint32
	FeaturesIncompat       uint32
	FeaturesLogIncompat    uint32

	// Icount is sb_icount: the number of inode slots the filesystem has
	// allocated, counted in chunks rather than individually. Ifree is sb_ifree,
	// how many of those slots are unused. The difference is the number of
	// inodes actually in use.
	//
	// Both are lazily maintained when LazySuperblockCounters reports true,
	// which is the default on any filesystem this parser is likely to meet.
	// The kernel then only writes them out on a clean unmount, so on an image
	// captured from a running or crashed system they are a hint, not a fact.
	// [Volume.AllocationGroupInodeInfo] carries the authoritative per-group
	// counters.
	Icount uint64
	Ifree  uint64

	// FeatureFlags2 is sb_features2. FeatureFlags2Backup is sb_bad_features2,
	// the duplicate the kernel keeps to work around a historical alignment bug.
	// They are normally identical; a difference means one of the two writes was
	// lost and is worth reporting.
	FeatureFlags2       uint32
	FeatureFlags2Backup uint32

	// SparseInodeAlignment is sb_spino_align, the sparse-inode allocation
	// granularity in filesystem blocks. It is zero unless the sparse-inodes
	// feature is enabled.
	SparseInodeAlignment uint32

	// Inodes the filesystem allocates for its own use. They are counted in
	// Icount and are perfectly real, but nothing in the directory tree ever
	// refers to them, so anything reconciling a directory walk against the
	// allocated set has to account for them separately or report them as
	// orphans. Each is zero or NULLFSINO when the filesystem has no such inode.
	RealtimeBitmapInodeNumber  uint64
	RealtimeSummaryInodeNumber uint64
	UserQuotaInodeNumber       uint64
	GroupQuotaInodeNumber      uint64
	ProjectQuotaInodeNumber    uint64
}

func (Superblock) AllocatedInodes added in v0.3.1

func (s Superblock) AllocatedInodes() uint64

AllocatedInodes returns the number of inodes in use: sb_icount minus sb_ifree.

"Allocated" here means an inode that exists and is owned by something, which is the sense a caller counting recoverable objects wants. It is not sb_icount, which counts allocated inode *slots* including the free ones.

The result is zero rather than a wrapped value when Ifree exceeds Icount, which can only happen on an image whose counters are damaged or stale. Use Superblock.LazySuperblockCounters to tell whether these numbers are trustworthy at all, and reconcile against the per-allocation-group counters before relying on them.

func (Superblock) HasBigTimestamps

func (s Superblock) HasBigTimestamps() bool

HasBigTimestamps reports whether inode timestamps use the 64-bit "bigtime" encoding rather than the legacy 32-bit seconds/nanoseconds pair.

func (Superblock) HasFeatureIncompat

func (s Superblock) HasFeatureIncompat(feature uint32) bool

HasFeatureIncompat reports whether an incompatible feature bit is set.

func (Superblock) HasLargeExtentCounts

func (s Superblock) HasLargeExtentCounts() bool

HasLargeExtentCounts reports whether inodes use the 64-bit extent counters introduced by the nrext64 feature.

func (Superblock) HasSparseInodes added in v0.3.1

func (s Superblock) HasSparseInodes() bool

HasSparseInodes reports whether inode chunks may be sparsely allocated. It changes the inode b-tree record layout: see InodeChunk.

func (Superblock) LazySuperblockCounters added in v0.3.1

func (s Superblock) LazySuperblockCounters() bool

LazySuperblockCounters reports whether Icount and Ifree are lazily maintained, and therefore only accurate as of the last clean unmount.

func (Superblock) MetadataInodeNumbers added in v0.3.1

func (s Superblock) MetadataInodeNumbers() []uint64

MetadataInodeNumbers returns the inodes the filesystem allocated for itself, in ascending order and with absent ones omitted.

These exist and are allocated, but no directory entry names them, which makes them the standing exception to "allocated but unreachable means deleted".

func (Superblock) NeedsRepair

func (s Superblock) NeedsRepair() bool

NeedsRepair reports whether the filesystem was marked as requiring repair. Such an image was left in an inconsistent state and its metadata should be treated with suspicion.

type UnlinkedInode added in v0.3.1

type UnlinkedInode struct {
	// InodeNumber is the absolute inode number, usable with
	// [Volume.OpenInode] and the rest of the API.
	InodeNumber uint64 `json:"inode_number"`
	// AllocationGroup is the group whose AGI names this chain, and Bucket is
	// which of its 64 chains the inode is on.
	AllocationGroup uint32 `json:"allocation_group"`
	Bucket          int    `json:"bucket"`
	// Position is how far along the chain the inode sits, counting from zero at
	// the head. The head is the most recently unlinked inode in that bucket.
	Position int `json:"position"`
}

UnlinkedInode is one inode found on an allocation group's unlinked chain.

type VerificationMode

type VerificationMode string

VerificationMode defines how report generation handles verification failures.

const (
	// VerificationModeBestEffort records anomalies and continues report generation.
	VerificationModeBestEffort VerificationMode = "best_effort"
	// VerificationModeStrict fails report generation on verification mismatch.
	VerificationModeStrict VerificationMode = "strict"
)

type Volume

type Volume struct {
	// contains filtered or unexported fields
}

Volume is an XFS volume parser with concurrency-safe read APIs.

func Open

func Open(reader io.ReaderAt) (*Volume, error)

Open parses an XFS volume from a random-access reader.

The reader is not closed by Volume.Close; the caller retains ownership. Use OpenVolumeFromPath when the volume should own its file handle. To read a filesystem embedded in a larger image, pass an io.SectionReader covering the partition.

The returned Volume is safe for concurrent use.

func OpenVolumeFromPath

func OpenVolumeFromPath(path string) (*Volume, error)

OpenVolumeFromPath opens an XFS volume from a filesystem path.

This is a convenience wrapper around os.Open and Open. The returned volume owns the underlying file handle, and Volume.Close will close it.

Raw device access generally requires elevated privileges: run as Administrator on Windows and use a path such as \\.\PhysicalDrive0, or read a block device such as /dev/sda1 on Linux.

func (*Volume) AllocationGroupCount added in v0.3.1

func (v *Volume) AllocationGroupCount() int

AllocationGroupCount returns the number of allocation groups whose inode headers were parsed when the volume was opened.

It is normally equal to the superblock's NumberOfAllocationGroups. Prefer it when indexing into Volume.AllocationGroupInodeInfo, because it describes what was actually read.

func (*Volume) AllocationGroupInodeInfo added in v0.3.1

func (v *Volume) AllocationGroupInodeInfo(index int) (InodeInformation, error)

AllocationGroupInodeInfo returns one allocation group's inode header.

This is the authoritative source for how many inodes a group holds, and the only place the unlinked-inode chain heads are exposed. Unlike the superblock counters it is updated with every inode allocation, so it stays correct on an image captured from a running system.

func (*Volume) AnalyzeInodeFragmentation

func (v *Volume) AnalyzeInodeFragmentation(inodeNumber uint64) (FragmentationReport, error)

AnalyzeInodeFragmentation analyzes the data-fork extent layout of an inode.

func (*Volume) AnalyzeInodeFragmentationByPath

func (v *Volume) AnalyzeInodeFragmentationByPath(path string) (FragmentationReport, error)

AnalyzeInodeFragmentationByPath resolves a file path then analyzes fragmentation.

func (*Volume) Close

func (v *Volume) Close() error

Close releases the volume.

It waits for in-flight reads to finish before releasing the backing reader, so it is safe to call concurrently with reads. Subsequent operations return ErrVolumeClosed. Closing an already closed volume returns ErrVolumeClosed.

func (*Volume) DirectoryArtifactReport

func (v *Volume) DirectoryArtifactReport(inodeNumber uint64) (DirectoryArtifactReport, error)

DirectoryArtifactReport reports active/deleted/carved records for one directory.

func (*Volume) DirectoryArtifactReportByPath

func (v *Volume) DirectoryArtifactReportByPath(path string) (DirectoryArtifactReport, error)

DirectoryArtifactReportByPath resolves a path and reports directory artifacts.

func (*Volume) DirectoryParentInode

func (v *Volume) DirectoryParentInode(inodeNumber uint64) (uint64, error)

DirectoryParentInode returns the inode number a directory's ".." refers to.

For short-form directories the parent is stored in the directory header; for block-backed directories it is the ".." entry in the first data block. It is the only in-inode link back up the tree, which makes it the starting point for reconstructing the path of an orphaned directory.

func (*Volume) DirectoryParentInodeByPath

func (v *Volume) DirectoryParentInodeByPath(path string) (uint64, error)

DirectoryParentInodeByPath resolves a directory path and returns its parent inode number.

func (*Volume) EnumerateAllocatedInodes added in v0.3.1

func (v *Volume) EnumerateAllocatedInodes(ctx context.Context, options InodeEnumerationOptions) (InodeEnumeration, error)

EnumerateAllocatedInodes returns every inode the filesystem has allocated, whether or not any directory refers to it.

This is the authoritative answer to what a volume contains. Comparing it against a directory walk is what turns "here is what we found" into "here is what we found, and here is the evidence that nothing else exists" — see Volume.InodeCompletenessReport, which does exactly that.

The returned inode numbers are ascending and unique. On a filesystem with many millions of inodes, prefer Volume.WalkInodeChunks, which streams.

func (*Volume) GetRootInode

func (v *Volume) GetRootInode() (*Inode, error)

func (*Volume) InodeCompletenessReport added in v0.3.1

func (v *Volume) InodeCompletenessReport(ctx context.Context,
	options InodeCompletenessOptions) (InodeCompletenessReport, error)

InodeCompletenessReport reconciles the directory tree against every inode the filesystem has allocated, and reports what the walk could not reach.

This is the self-check that makes a file listing defensible. It needs no mount, no kernel support and no second tool: the evidence is already on the volume, in three places that are maintained separately and therefore corroborate each other.

Memory is bounded by the number of reachable inodes plus the number of orphans, not by the total inode count, because the allocated set is streamed rather than materialised.

func (*Volume) InodeForensicReport

func (v *Volume) InodeForensicReport(inodeNumber uint64) (InodeForensicReport, error)

InodeForensicReport builds a structured report for one inode.

func (*Volume) InodeForensicReportByPath

func (v *Volume) InodeForensicReportByPath(path string) (InodeForensicReport, error)

InodeForensicReportByPath resolves a path and reports that inode.

func (*Volume) IsClosed

func (v *Volume) IsClosed() bool

IsClosed reports whether the volume has been closed.

It is a point-in-time answer: on a volume shared with a goroutine that may call Close, prefer acting on ErrVolumeClosed from the operation itself.

func (*Volume) ListDirectoryEntries

func (v *Volume) ListDirectoryEntries(inodeNumber uint64) ([]DirectoryEntry, error)

ListDirectoryEntries lists active entries for a directory inode.

Short-form, block, leaf, node and btree directories are all supported: entries always live in the directory's data-block region, which is walked one directory block at a time. Entries recovered before a failure are returned alongside the error: on a damaged image the blocks that did parse are evidence, and discarding them because a later block did not is how a recursive walk loses whole subtrees. Use ListDirectoryEntriesReport when the completeness of the listing matters.

func (*Volume) ListDirectoryEntriesByPath

func (v *Volume) ListDirectoryEntriesByPath(path string) ([]DirectoryEntry, error)

ListDirectoryEntriesByPath resolves a directory path and lists its entries.

func (*Volume) ListDirectoryEntriesReport added in v0.3.0

func (v *Volume) ListDirectoryEntriesReport(inodeNumber uint64) (DirectoryListing, error)

ListDirectoryEntriesReport lists a directory and reports how the scan went.

ListDirectoryEntries returns only the entries, so a caller cannot tell a complete listing from one that stopped at a cap or skipped an unreadable block. The returned DirectoryListing carries Truncated, Anomalies, BlocksScanned and SourceFormat for callers that must know.

func (*Volume) ListDirectoryEntriesReportByPath added in v0.3.0

func (v *Volume) ListDirectoryEntriesReportByPath(path string) (DirectoryListing, error)

ListDirectoryEntriesReportByPath resolves a path and lists it with a report.

func (*Volume) ListDirectoryEntriesWithOptions

func (v *Volume) ListDirectoryEntriesWithOptions(inodeNumber uint64, options DirectoryScanOptions) (DirectoryListing, error)

ListDirectoryEntriesWithOptions lists a directory under explicit scan options.

func (*Volume) ListInodeExtendedAttributes

func (v *Volume) ListInodeExtendedAttributes(inodeNumber uint64) ([]ExtendedAttribute, error)

ListInodeExtendedAttributes lists decoded inode extended attributes.

For block-based attribute trees, returned entries preserve traversal order. If duplicate fully-qualified names are present, duplicates are preserved in the returned slice (no deduplication is performed).

func (*Volume) ListRootDirectoryEntries

func (v *Volume) ListRootDirectoryEntries() ([]DirectoryEntry, error)

ListRootDirectoryEntries lists entries for the root directory inode.

func (*Volume) OpenInode

func (v *Volume) OpenInode(inodeNumber uint64) (*Inode, error)

func (*Volume) OpenInodeByPath

func (v *Volume) OpenInodeByPath(path string) (*Inode, error)

OpenInodeByPath resolves an absolute path and opens the corresponding inode.

func (*Volume) ReadFileData

func (v *Volume) ReadFileData(inodeNumber uint64) ([]byte, error)

ReadFileData reads all data bytes from a non-directory inode.

func (*Volume) ReadFileDataByPath

func (v *Volume) ReadFileDataByPath(path string) ([]byte, error)

ReadFileDataByPath resolves an absolute path and reads all file data bytes.

func (*Volume) ReadInodeAttributeForkData

func (v *Volume) ReadInodeAttributeForkData(inodeNumber uint64) ([]byte, error)

ReadInodeAttributeForkData reads the inode attributes fork payload. For inline attributes, this returns the inline bytes; for extent/btree forks, it reconstructs data from mapped extents up to the attributes fork size.

func (*Volume) ReadInodeData

func (v *Volume) ReadInodeData(inodeNumber uint64, p []byte, off int64) (int, error)

ReadInodeData reads file data from an inode at offset. It supports inline data and extent-list backed regular files.

func (*Volume) Report

func (v *Volume) Report() (*XFSReport, error)

Report builds a combined report from root path "/".

func (*Volume) ReportWithContext

func (v *Volume) ReportWithContext(ctx context.Context, options ReportOptions) (*XFSReport, error)

ReportWithContext builds a combined forensic report, honouring context cancellation.

Discovery of the inode set is sequential, because a directory must be read before its children are known. Per-inode analysis then runs across the pool configured by options.Concurrency. The result does not depend on the worker count: entries are stored by position and sorted before returning.

func (*Volume) ReportWithOptions

func (v *Volume) ReportWithOptions(options ReportOptions) (*XFSReport, error)

ReportWithOptions builds a combined forensic report.

It is equivalent to ReportWithContext with a background context.

func (*Volume) ResolveInodeByPath

func (v *Volume) ResolveInodeByPath(path string) (uint64, error)

ResolveInodeByPath resolves an absolute path to an inode number.

func (*Volume) ScanDirectoryRecords

func (v *Volume) ScanDirectoryRecords(inodeNumber uint64) ([]DirectoryRecord, error)

ScanDirectoryRecords lists active and deleted directory records for an inode.

Records carrying IsCarved are probabilistic: see DirectoryRecord.Kind and IsProbabilistic before presenting them as fact.

func (*Volume) ScanDirectoryRecordsByPath

func (v *Volume) ScanDirectoryRecordsByPath(path string) ([]DirectoryRecord, error)

ScanDirectoryRecordsByPath resolves a directory path and scans its records.

func (*Volume) ScanDirectoryRecordsByPathWithOptions

func (v *Volume) ScanDirectoryRecordsByPathWithOptions(path string, options DirectoryScanOptions) (DirectoryListing, error)

ScanDirectoryRecordsByPathWithOptions resolves a path and scans it.

func (*Volume) ScanDirectoryRecordsWithOptions

func (v *Volume) ScanDirectoryRecordsWithOptions(inodeNumber uint64, options DirectoryScanOptions) (DirectoryListing, error)

ScanDirectoryRecordsWithOptions scans a directory for active, free and carved records under explicit scan options.

func (*Volume) Superblock

func (v *Volume) Superblock() Superblock

func (*Volume) UnlinkedInodes added in v0.3.1

func (v *Volume) UnlinkedInodes(ctx context.Context) ([]UnlinkedInode, []ReportAnomaly, error)

UnlinkedInodes returns every inode on an AGI unlinked chain, in allocation group, bucket and chain order.

These are files that were deleted while still open. They are allocated, intact and fully readable, and no directory refers to them, so a walk of the directory tree cannot find them. The returned anomalies describe any chain that could not be followed to its end.

func (*Volume) VerifyDirectoryIndex

func (v *Volume) VerifyDirectoryIndex(inodeNumber uint64) (DirectoryIndexReport, error)

VerifyDirectoryIndex cross-checks a directory's hash index against its data blocks. Directories with no index report HasIndex false and are consistent by definition.

func (*Volume) VerifyDirectoryIndexByPath

func (v *Volume) VerifyDirectoryIndexByPath(path string) (DirectoryIndexReport, error)

VerifyDirectoryIndexByPath resolves a path and verifies its hash index.

func (*Volume) VolumeIntegrityReport

func (v *Volume) VolumeIntegrityReport() (VolumeIntegrityReport, error)

VolumeIntegrityReport builds a metadata/geometry report for the open volume.

func (*Volume) WalkInodeChunks added in v0.3.1

func (v *Volume) WalkInodeChunks(ctx context.Context, options InodeEnumerationOptions,
	fn func(InodeChunk) error) (InodeEnumeration, error)

WalkInodeChunks calls fn for every inode b-tree record on the volume, in allocation group then inode number order.

It is the streaming form of Volume.EnumerateAllocatedInodes: nothing is retained between calls, so a filesystem with hundreds of millions of inodes costs a bounded amount of memory. Returning an error from fn stops the walk and returns that error, with the partially populated result.

The returned InodeEnumeration carries the counts and anomalies but an empty Inodes, since the caller has already seen every record.

type VolumeError

type VolumeError struct {
	Op  string
	Err error
}

func (*VolumeError) Error

func (e *VolumeError) Error() string

func (*VolumeError) Unwrap

func (e *VolumeError) Unwrap() error

type VolumeIntegrityReport

type VolumeIntegrityReport struct {
	Type                     string          `json:"type"`
	FormatVersion            uint8           `json:"format_version"`
	BlockSize                uint32          `json:"block_size"`
	InodeSize                uint16          `json:"inode_size"`
	DirectoryBlockSize       uint32          `json:"directory_block_size"`
	RootDirectoryInodeNumber uint64          `json:"root_directory_inode"`
	AllocationGroupSize      uint32          `json:"allocation_group_size"`
	NumberOfAllocationGroups uint32          `json:"number_of_allocation_groups"`
	NumberOfBlocks           uint64          `json:"number_of_blocks"`
	VolumeLabel              string          `json:"volume_label,omitempty"`
	SuperblockCRCChecked     bool            `json:"superblock_crc_checked"`
	SuperblockCRCValid       bool            `json:"superblock_crc_valid"`
	Anomalies                []ReportAnomaly `json:"anomalies,omitempty"`
}

VolumeIntegrityReport summarizes core XFS geometry and validation findings.

type XFSReport

type XFSReport struct {
	GeneratedAt        time.Time                 `json:"generated_at"`
	RootPath           string                    `json:"root_path"`
	Provenance         ReportProvenance          `json:"provenance"`
	Volume             VolumeIntegrityReport     `json:"volume"`
	Files              []InodeForensicReport     `json:"files"`
	DirectoryArtifacts []DirectoryArtifactReport `json:"directory_artifacts,omitempty"`
	// Completeness is present when ReportOptions.IncludeInodeCompleteness asked
	// for it. It is what turns Files from a list of what was found into a list
	// with evidence that nothing else exists.
	Completeness *InodeCompletenessReport `json:"completeness,omitempty"`
	Anomalies    []ReportAnomaly          `json:"anomalies,omitempty"`
}

XFSReport is a combined volume + inode + directory-artifact report.

func (*XFSReport) Summary

func (r *XFSReport) Summary() string

Summary returns a human-readable summary of the report.

Directories

Path Synopsis
examples
basic command
completeness command
Command completeness reconciles a directory walk against every inode an XFS volume has allocated, and lists the ones no directory reaches.
Command completeness reconciles a directory walk against every inode an XFS volume has allocated, and lists the ones no directory reaches.
dirscan command
extract command
forensics command
fragmentation command
inode_read command
report command
traverse command
xattrs command
tools
corpus/oracle command
Command oracle walks a mounted filesystem and emits a canonical record for every path it contains.
Command oracle walks a mounted filesystem and emits a canonical record for every path it contains.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL