filesystem_exfat

package module
v0.4.1 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: 17 Imported by: 4

README

go-filesystems/exfat

exfat

Go Reference CI

Pure-Go read/write access to exFAT filesystem images — no root privileges, no external tools, no CGO.

Supports bare filesystem images and MBR/GPT partitioned disks, full directory traversal, file mutation and filesystem creation.

Support summary

Feature Status Notes
Open / Close ✅ Supports bare images and partitioned disks
Format ✅ Creates exFAT images
ReadFile ✅ Full file reads supported
Read at an offset ✅ OpenFile(path) → io.ReaderAt + Size() (filesystem.Opener / filesystem.File)
Write at an offset ✅ OpenFile(path) → io.WriterAt + Truncate + Sync (filesystem.WritableFile). Writes only the bytes given; a length change materialises a NoFatChain run into a real FAT chain first
WriteFile ✅ Full file writes supported
MkDir / Delete / Rename ✅ Directory operations supported
ReadLink / Symlinks ⚠️ No exFAT does not support POSIX symlinks
Volume label ✅ Label / SetLabel (filesystem.Labeller)
Truncate ✅ Truncate(path, newSize) (filesystem.Truncater)
Grow / Shrink / Resize ✅ Format provisions FAT headroom so Grow doesn't immediately need relocation
Partitioned images ✅ MBR/GPT supported

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 cluster buffer is allocated only after the cluster's last byte has been read: a 512-byte file whose boot sector declares 32 MiB clusters no longer costs 64 MiB to Open;
  • a NoFatChain (contiguous) run is bounded by the bytes the image really holds, not by ClusterCount and the entry's DataLength, so two small claims cannot size a multi-gigabyte cluster list;
  • a directory is at most 256 MiB (8388608 entries of 32 bytes, Linux MAX_EXFAT_DENTRIES), not 256 clusters of up to 32 MiB;
  • an allocation bitmap larger than the image is refused.

When the reader cannot say how long it is, the length is found by probing single bytes (about 2*log2(size) reads).

FuzzOpenAndWalk drives the read surface; the regression tests are in bombs_test.go.

Limitations

  • exFAT does not support POSIX symlinks or POSIX permissions/ACLs.
  • Metadata is limited compared to POSIX filesystems (no ownership, no Unix permissions).
  • No journaling; this implementation is intended for tooling and tests, not production workloads.

Module

github.com/go-filesystems/exfat

Supported operations

Operation Status
Open / Close ✅ implemented
Format ✅ implemented
Stat ✅ implemented
ListDir ✅ implemented
ReadFile ✅ implemented
OpenFile ✅ implemented (filesystem.Opener)
WriteAt ✅ implemented (filesystem.WritableFile, on the File)
WriteFile ✅ implemented
MkDir ✅ implemented
DeleteFile ✅ implemented
DeleteDir ✅ implemented (recursive)
Rename ✅ implemented
ReadLink ⚠️ stub — exFAT has no symlinks

API

Open and Format return the plain filesystem.Filesystem interface (there is no richer exported FS type) — exFAT-specific extras (Info, PartitionOffset, volume label, truncate, resize) are reached by type-asserting to the optional interfaces from github.com/go-filesystems/interface, or to the package's own Info getter.

Format / Open
type FormatConfig struct {
    Label              string
    VolumeSerialNumber uint32 // 0 = randomly generated
}

func Format(path string, sizeBytes int64, cfg FormatConfig) (filesystem.Filesystem, error)
func Open(imagePath string, partIndex int) (filesystem.Filesystem, error)
Read / Write

The returned value's Stat / ListDir / ReadFile / WriteFile / MkDir / DeleteFile / DeleteDir / Rename are exactly the filesystem.Filesystem contract — see that package's README for the full signatures.

exFAT-specific extras (type-assert)
// Read part of a file without materialising all of it: OpenFile resolves the
// FAT chain once (cluster numbers, not data) and then serves byte ranges, so a
// 4 KiB read out of a 4 GiB file costs 4 KiB. Required by anything that mounts
// or exports the volume.
if o, ok := fs.(filesystem.Opener); ok {
    f, err := o.OpenFile("/big.bin")
    if err != nil {
        return err
    }
    defer f.Close()
    buf := make([]byte, 4096)
    n, err := f.ReadAt(buf, 1<<30) // io.ReaderAt semantics, exactly
    _, _ = n, err
    _ = f.Size()                   // from metadata; reads nothing

    // ...and write part of one, in place. Without this a positional write can
    // only be ReadFile + splice + WriteFile: the whole file read, reallocated
    // and written back on every request, so writing a file in blocks is
    // quadratic. Measured over a real Linux kernel NFS mount, 8 MiB in 64 KiB
    // blocks took 31 s (269 kB/s) that way, against 1.5 s with this.
    if w, ok := f.(filesystem.WritableFile); ok {
        _, err = w.WriteAt(buf, 1<<30) // io.WriterAt semantics, exactly
        _ = w.Truncate(1 << 20)        // grow zero-fills; shrink frees clusters
        _ = w.Sync()                   // fsync(2) on a file-backed image
    }
}
if l, ok := fs.(filesystem.Labeller); ok {
    _ = l.SetLabel("MYVOL")
}
if t, ok := fs.(filesystem.Truncater); ok {
    _ = t.Truncate("/big.bin", 4096)
}
if r, ok := fs.(interface{ Resize(int64) error }); ok {
    _ = r.Resize(newSizeBytes) // also exposes Grow(int64) error / Shrink(int64) error
}
if i, ok := fs.(interface{ Info() Info }); ok {
    fmt.Println(i.Info().ClusterSize())
}

Implements

This package implements the filesystem.Filesystem interface defined in github.com/go-filesystems/interface. Callers can treat the value returned by Open/Format as a filesystem.Filesystem to write generic tooling that works across the other filesystem modules in this repository.

Example:

import (
    filesystem "github.com/go-filesystems/interface"
    fsex "github.com/go-filesystems/exfat"
)

f, _ := fsex.Open("exfat.img", -1)
defer f.Close()
var fs filesystem.Filesystem = f
_, _ = fs.ReadFile("/hello.txt")

Documentation

Index

Constants

View Source
const MaxLabelLen = 11

MaxLabelLen is the maximum number of UTF-16 code units storable in the exFAT Volume Label entry (per the exFAT specification §7.3).

Variables

View Source
var ErrReadOnlyReader = errors.New("exfat: 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.

Functions

func Format

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

Format creates a new exFAT filesystem in the file at path. The file is created (or truncated) and formatted. sizeBytes must be a multiple of the cluster size (4096) and large enough to hold the metadata region plus at least one data cluster.

On success the newly formatted filesystem is opened and returned; the caller must Close it when done.

func Open

func Open(imagePath string, partIndex int) (filesystem.Filesystem, error)

Open opens imagePath, optionally selecting a partition, and parses the exFAT boot sector.

func OpenReader added in v0.4.0

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

OpenReader opens the exFAT filesystem 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 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 FormatConfig

type FormatConfig struct {
	// Label is the volume label (stored as a volume-label directory entry;
	// not in the boot sector for exFAT). Trimmed to 11 characters.
	Label string
	// VolumeSerialNumber is the 32-bit serial number. A random value is generated when zero.
	VolumeSerialNumber uint32
}

FormatConfig holds optional parameters for Format. All fields are optional; sensible defaults are used when left at their zero value.

type Info

type Info struct {
	PartitionStartSector   uint64
	VolumeLength           uint64
	FATOffset              uint32
	FATLength              uint32
	ClusterHeapOffset      uint32
	ClusterCount           uint32
	RootDirectoryCluster   uint32
	VolumeSerialNumber     uint32
	FileSystemRevision     uint16
	VolumeFlags            uint16
	BytesPerSectorShift    uint8
	SectorsPerClusterShift uint8
	NumberOfFATs           uint8
	DriveSelect            uint8
	PercentInUse           uint8
}

Info holds the fields decoded from the exFAT main boot sector.

func (Info) BytesPerSector

func (info Info) BytesPerSector() uint32

BytesPerSector returns the logical sector size in bytes.

func (Info) ClusterHeapOffsetBytes

func (info Info) ClusterHeapOffsetBytes(partOffset int64) int64

ClusterHeapOffsetBytes returns the absolute byte offset of the cluster heap.

func (Info) ClusterSize

func (info Info) ClusterSize() uint64

ClusterSize returns the allocation cluster size in bytes.

func (Info) FATOffsetBytes

func (info Info) FATOffsetBytes(partOffset int64) int64

FATOffsetBytes returns the absolute byte offset of the first FAT.

func (Info) RootDirOffset

func (info Info) RootDirOffset(partOffset int64) int64

RootDirOffset returns the absolute byte offset of the root directory cluster.

func (Info) SectorsPerCluster

func (info Info) SectorsPerCluster() uint32

SectorsPerCluster returns the cluster size expressed in sectors.

Jump to

Keyboard shortcuts

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