Filesystem interfaces

FileSystem and Entry

type FileSystem interface {
    Type() Type
    Root() Entry
    Open(path string) (Entry, error)
}

type Entry interface {
    Path() string
    IsDir() bool
    Children() ([]Entry, error) // empty for non-directories
    NamedStreams() ([]string, error)
    // slack-space and timestomp access are exposed via optional
    // interfaces (below), not required on every Entry.
}

How commands use them

Children() is what makes the targetless detect possible.

The test double

internal/filesystem/fakefs is an in-memory FileSystem and Entry. It implements all three capabilities and includes an in-memory Image. The command and technique tests run against it.

The registry

registry.go holds a signature-based lookup. It is a []Detector, where each detector pairs a byte-pattern sniff with a constructor. It returns the right FileSystem for an image.

To add a filesystem, create an internal/filesystem/<fs>/ package and register a Detector from its init. Nothing in internal/technique or the commands changes.

Register panics on:

Registration happens at package init, so these are programmer errors at startup. They are not conditions to recover from.

Open reads up to the first 4096 bytes and tries every registered detector. It does not stop at the first hit.

Capability interfaces

Filesystems differ in what they support. Rather than force every Entry to satisfy one bloated interface, capabilities are split into optional interfaces that an Entry may also implement:

type NamedStreamCapable interface {
    WriteStream(name string, data []byte) error
    ReadStream(name string) ([]byte, error)
    DeleteStream(name string) error
}

type SlackSpaceCapable interface {
    SlackRegions() ([]SlackRegion, error)
}

type TimestompCapable interface {
    Timestamp(field TimeField) (time.Time, error)
    SupportsTimestamp(field TimeField) (bool, error)
    SetTimestamp(field TimeField, t time.Time) error
}

internal/technique checks whether the Entry implements the capability it needs. If not, it returns an “unsupported on this filesystem” error.

ext4 image entries implement all three. A capability can still return ErrUnsupported for an entry type it cannot handle, such as slack space on a directory. Default scans skip that technique for the entry and keep walking.

Technique selection and the features matrix

Commands choose a technique only from the --technique string. A switch in internal/technique does the mapping, and no filesystem is involved:

func Get(name string) (Technique, error) // "named-stream" | "slack-space" | "timestomp"

Whether that technique works on the target is answered later, at call time, by the capability check. The Entry either implements the capability or it does not.

So there is no lookup table keyed by (FileSystem type, technique). Support follows from whether ntfs.Entry, apfs.Entry, or ext4.Entry implements the capability.

How features works

nemo features needs the matrix without an image loaded. It cannot run the capability check on a live Entry. So each filesystem package declares its supported technique names once, in the Detector it registers:

type Detector struct {
    Type       Type
    Sniff      func([]byte) bool
    New        func(image.Image) (FileSystem, error)
    Techniques []string // ext4: []string{"named-stream", "slack-space", "timestomp"}
}

features reads Techniques from every registered Detector and prints the matrix. It needs no image and no reflection. It cannot drift from registry.go, because it reads the same struct detection uses.

Filesystem implementations must keep their declared techniques consistent with their capabilities. See features for the matrix.