filesystem_ntfs

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: BSD-3-Clause Imports: 15 Imported by: 4

README

go-filesystems/ntfs

ntfs package

Go Reference CI

This package provides a minimal NTFS driver that implements the filesystem.Filesystem interface.

Implementation details:

  • The driver stores a small header region at the start of the image and places file blobs after that header. All operations (read/write/mkdir/rename) operate inside the image file rather than on the host filesystem.
  • This is a lightweight, test-oriented implementation and does not attempt to implement the full NTFS on-disk format. Use a real NTFS implementation for full compatibility or integration tests.

Open sniffs the boot sector: genuine NTFS volumes are handed to a read-only on-disk reader (real_ntfs.go / real_ntfs_features.go) that parses real structures straight off disk; the custom NTFSIMG1 blob format keeps using the legacy read/write code path.

Support summary — NTFSIMG1 (in-image mock, read/write)

Feature Status Notes
Open / Close ✅ Minimal in-image implementation
ReadFile / WriteFile ✅ Blob storage model inside image
MkDir / Delete / Rename ✅ Implemented (recursive delete supported)
Symlink (create) ✅ NTFSIMG1 in-image driver
ReadLink (read target) ✅ NTFSIMG1 metadata
Free-list reuse ✅ In-image free-list and reuse implemented
Volume label ✅ Label / SetLabel (filesystem.Labeller)
Grow / Shrink / Resize ✅ FS.Grow / FS.Shrink / FS.Resize resize the NTFSIMG1 image in place; NOT real on-disk NTFS resize (real reader returns an error)
Compaction ✅ FS.Compact relocates blobs to remove fragmentation and rebuilds the free-list; FS.FragmentationStats / FS.Layout report usage and layout

Support summary — real on-disk NTFS (read-only)

Feature Status Notes
Boot sector / $MFT / FILE records ✅ USA fixup, attribute walk, fragmented $MFT runlist
Resident / non-resident $DATA ✅ Data-run (runlist) decoding
Directory listing ✅ $INDEX_ROOT + $INDEX_ALLOCATION (INDX)
LZNT1 compression ✅ Per-compression-unit decode; validated byte-exact vs ntfs-3g
Sparse files ✅ Sparse runs read back as zeroes
Named data streams (ADS) ✅ StreamReader: ListStreams / ReadStream
Symlinks / junctions ✅ $REPARSE_POINT (IO_REPARSE_TAG_SYMLINK / IO_REPARSE_TAG_MOUNT_POINT) and ntfs-3g IntxLNK; ReadLink
$ATTRIBUTE_LIST (spanning records) ✅ Extension records stitched; split runlists merged by VCN
Volume label ✅ Label reads $VOLUME_NAME
Write path ⚠️ No Read-only reader; mutating methods return an error
EFS-encrypted data ⚠️ No Not decodable without the user's keys (clear error)

Named streams are read through the package's StreamReader capability:

if sr, ok := fs.(ntfs.StreamReader); ok {
	names, _ := sr.ListStreams("/notes.txt")     // e.g. ["", "author"]
	author, _ := sr.ReadStream("/notes.txt", "author")
	_ = author
}

Notes on current capabilities:

  • Path normalization: paths are normalized and stored with a leading /.
  • Parent directories: WriteFile will create any missing parent directories.
  • Directory rename: renaming a directory updates all nested entries.
  • Deletion: DeleteDir removes a directory and its contents recursively.

Untrusted input

An image is attacker-controlled data. Every allocation a value in the image can size is bounded before it happens, and a hostile image gets an error, not an out-of-memory kill:

  • a data run must lie inside the image (the last byte of every allocated span is read before the buffer is made), and metadata attributes ($INDEX_ALLOCATION, $ATTRIBUTE_LIST, $REPARSE_POINT) may not be sparse;
  • a compressed stream is sized by its runlist, not by the realSize claim, and a compression unit is at most 64 KiB (Linux ntfs3: 16 clusters of at most 4096 bytes);
  • index entries must be at least 0x10 bytes with the key inside the entry (Linux ntfs3 hdr_find_e), so overlapping keys cannot multiply the output;
  • ReadLink does not read a file larger than a path to look for the IntxLNK marker;
  • sparse $DATA legitimately reads back bigger than the image, so it has a ceiling: DefaultMaxDataSize (1 GiB), lowered with SetMaxDataSize on the DataLimiter that Open returns. A read over it fails with an error that wraps ntfs.ErrTooLarge before anything is allocated.
if l, ok := fsys.(ntfs.DataLimiter); ok {
    l.SetMaxDataSize(64 << 20)
}

FuzzRealNTFS and FuzzOpen drive the read surface; the regression tests are in bombs_test.go.

Limitations

  • The NTFSIMG1 in-image driver does not implement NTFS metadata (ACLs, journaling, or real on-disk structures); the real on-disk reader above is read-only and does not decode EFS-encrypted data.
  • Grow/Shrink/Resize and Compact operate on the NTFSIMG1 image format only; the real on-disk NTFS reader is read-only and returns an error for all of them (resizing a real NTFS volume would require manipulating the MFT, $Bitmap, and runlists — a separate, much larger project).

Free-list, reuse, and compaction

  • The driver maintains an in-image free-list of blob extents freed by delete operations and reuses those extents for subsequent writes when a suitably-sized extent is available. Contiguous free extents are coalesced to reduce fragmentation.
  • FS.Compact() relocates blobs to remove fragmentation and rebuilds the free-list as a single tail extent; FS.FragmentationStats() reports file count, bytes used, free-extent count, total free bytes, and largest free extent; FS.Layout() returns the current per-file blob layout.

Future improvements

  • Add more NTFS metadata (timestamps, inodes, ACLs) for better compatibility.

Implements

This package implements the filesystem.Filesystem interface from the github.com/go-filesystems/interface module. Tools that accept the filesystem.Filesystem abstraction can operate on NTFS images created or opened by this package.

Example:

import (
	filesystem "github.com/go-filesystems/interface"
	fsnt "github.com/go-filesystems/ntfs"
)

f, _ := fsnt.Open("ntfs.img", -1)
defer f.Close()
var fs filesystem.Filesystem = f
_, _ = fs.Stat("/")

Documentation

Index

Constants

View Source
const DefaultMaxDataSize = maxDataSize

DefaultMaxDataSize is the most bytes one ReadFile / ReadStream / directory read will materialise from a real NTFS volume unless SetMaxDataSize says otherwise. A sparse file legitimately reads back as far more bytes than the image holds, so this is a ceiling on the amplification, not a structural bound; the structural bounds (a run must lie inside the image, metadata is never sparse) are applied first.

View Source
const MaxLabelLen = 128

MaxLabelLen is the cap we enforce on the volume label. The real NTFS limit is 32 UTF-16 code units (~64 on-disk bytes); we use a slightly generous byte cap here since this mock driver stores raw bytes — that gives users headroom while still rejecting absurdly long input.

Variables

View Source
var ErrReadOnlyReader = errors.New("ntfs: this filesystem was opened over a reader that cannot be written")

ErrReadOnlyReader is what a write gets when the filesystem was opened over something that can only be read.

View Source
var ErrShrinkTooSmall = errors.New("ntfs: shrink target smaller than highest used offset")

ErrShrinkTooSmall is returned by Shrink and Resize when the requested new size is smaller than the highest byte currently occupied by a live file blob. Callers must Compact or delete files before retrying.

View Source
var ErrTooLarge = safeio.ErrTooLarge

ErrTooLarge is wrapped by the error a read returns when the data it would have to materialise exceeds the ceiling. Test it with errors.Is.

Functions

func Format

func Format(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)

func OpenReader added in v0.2.0

func OpenReader(r io.ReaderAt, size int64) (filesystem.Filesystem, error)

OpenReader opens the NTFS volume that starts at offset zero of r.

It exists so this driver fits the one shape the rest of go-filesystems agrees on: github.com/go-filesystems/detect's Opener is func(io.ReaderAt, int64) (filesystem.Filesystem, error), and until now this driver could only be opened from a PATH. A caller with an image already open -- a partition inside a disk image, a section of a larger file, an image in memory -- had to write it to a temporary file first, and detect.Register could not be given this driver at all.

There is no partition index: r is expected to begin at the filesystem, which is what detect has just established by reading the magic there. Open remains the way to look through a partition table.

WRITES need an io.WriterAt. A reader that is only a reader gives a filesystem that serves every read and refuses every write with ErrReadOnlyReader -- rather than one that accepts a write and loses it.

The richer FS interface this package's Open returns is still there: a caller that wants Layout, Compact or the fragmentation statistics can assert for it. What is returned here is filesystem.Filesystem because that is what detect.Opener is, and fitting it is the point.

The reader is NOT closed by Close: the caller opened it and still owns it. That is the same division as iso9660, squashfs, hfsplus and ufs, whose reader-based constructors have always worked this way.

Types

type DataLimiter added in v0.3.0

type DataLimiter interface {
	// SetMaxDataSize sets the most bytes one read may allocate. n <= 0
	// restores DefaultMaxDataSize. A read over the ceiling fails with an error
	// wrapping ErrTooLarge before anything is allocated.
	SetMaxDataSize(n int64)
}

DataLimiter is implemented by the filesystem Open and OpenReader return for a genuine NTFS volume. A caller reading untrusted images asserts for it to lower the per-read ceiling:

if l, ok := fs.(ntfs.DataLimiter); ok {
    l.SetMaxDataSize(64 << 20)
}

type FS

type FS interface {
	filesystem.Filesystem
	FragmentationStats() (files int, used uint64, freeExtents int, totalFree uint64, largestFree uint64)
	Layout() []FileLayout
	Compact() error
	Symlink(target, linkPath string) error
	Label() string
	SetLabel(label string) error
	Grow(newSizeBytes int64) error
	Shrink(newSizeBytes int64) error
	Resize(newSizeBytes int64) error
}

FS is the public interface returned by Open. It extends filesystem.Filesystem with NTFS-specific operations (fragmentation analysis, compaction and NTFSIMG1 image resize). Callers that only need the generic interface can use it directly; callers that want the extras type-assert (or accept *FS via Open's return type).

NOTE: Grow / Shrink / Resize operate on the NTFSIMG1 image format this driver implements, NOT on real NTFS. Resizing a real NTFS volume would require manipulating the MFT, $Bitmap and runlists and is a separate, much larger project.

func Open

func Open(imagePath string, partIndex int) (FS, error)

Open opens the image file and loads the in-image index. The driver stores a fixed-size header at the start of the partition and places file blobs after that header. partIndex is currently ignored (assumed single-image usage).

type FileLayout

type FileLayout struct {
	Path   string
	Offset uint64
	Size   uint64
}

FileLayout describes a stored file blob location inside the image.

type FormatConfig

type FormatConfig struct{ Label string }

Format creates a fresh NTFS-like image at path with the given size.

type StreamReader

type StreamReader interface {
	// ListStreams returns the names of every $DATA stream on the file, with
	// the unnamed default stream represented by "". Names are sorted and
	// de-duplicated.
	ListStreams(path string) ([]string, error)
	// ReadStream returns the full content of the named $DATA stream (name ""
	// selects the default stream), transparently decompressing LZNT1 data.
	ReadStream(path, name string) ([]byte, error)
}

StreamReader is the optional capability, satisfied by the real-NTFS reader, for enumerating and reading a file's named data streams (alternate data streams). The unnamed default stream is reported as "".

if sr, ok := fs.(ntfs.StreamReader); ok {
    names, _ := sr.ListStreams("/notes.txt")   // ["", "author"]
    b, _ := sr.ReadStream("/notes.txt", "author")
}

Jump to

Keyboard shortcuts

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