Help us shape the future of Go! Take the 2026 Go Developer Survey

portable

package
v1.4.13 Latest Latest
Warning

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

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package portable reads and writes the vendor-neutral nself portable export bundle (contract:cli.portable-export v1, P7-ADOPT-23).

A bundle is a directory: manifest.json plus the member files it lists. The Writer produces a deterministic manifest; the Reader treats the directory as untrusted input and refuses an unknown major version (E515) or any integrity failure (E516): a missing, changed, truncated, oversized, unlisted, duplicated or unsafely named member, a member that is no longer the file that was verified, and any symlink or hard link. Member names are Unicode NFC and unique under case folding; storage objects are content-addressed (StorageMember).

Layering: L1; imports only internal/errs, the standard library and golang.org/x/text (NFC and case folding).

Index

Constants

View Source
const (
	// SHA256Pattern matches a lowercase hex SHA-256.
	SHA256Pattern = `^[0-9a-f]{64}$`
	// HashPattern matches a table hash in canonical form: a decimal integer
	// with no sign on zero, no leading zeros and no "+" ("0", "7", "-12").
	// Hashes are compared as strings, so one number has one spelling.
	HashPattern = `^(0|-?[1-9][0-9]*)$`
	// VersionPattern matches a v1 schema_version ("1", "1.2"): major 1 only.
	VersionPattern = `^1(\.[0-9]+)*$`
)

Patterns shared with the generated JSON schema.

View Source
const Format = "nself-portable-export"

Format is the manifest "_format" marker.

View Source
const MajorVersion = 1

MajorVersion is the only major this package reads and writes.

View Source
const ManifestName = "manifest.json"

ManifestName is the manifest file name at the bundle root.

View Source
const SchemaVersion = "1"

SchemaVersion is the manifest version the Writer emits. The Reader accepts any "1" or "1.<n>" and refuses every other major (E515).

View Source
const StorageMemberPattern = `^storage/objects/[0-9a-f]{64}$`

StorageMemberPattern matches a storage member path.

View Source
const StorageMemberPrefix = "storage/objects/"

StorageMemberPrefix is the directory of every stored object's bytes.

Variables

View Source
var (
	// ErrUnknownMajor: the manifest is not a v1 portable export (E515).
	ErrUnknownMajor = errors.New("unknown portable bundle format or major version")
	// ErrChecksum: a member's SHA-256 differs from the manifest (E516).
	ErrChecksum = errors.New("bundle file checksum mismatch")
	// ErrMissing: a listed member is absent (E516).
	ErrMissing = errors.New("bundle file missing")
	// ErrSize: a member's length differs from the manifest, or a size limit
	// was exceeded (E516).
	ErrSize = errors.New("bundle file size mismatch or limit exceeded")
	// ErrDuplicate: two members share a path (E516).
	ErrDuplicate = errors.New("duplicate bundle member")
	// ErrUnlisted: an entry exists in the bundle directory that the manifest
	// does not list (E516).
	ErrUnlisted = errors.New("bundle entry not listed in the manifest")
	// ErrLink: a member is a symlink, a hard link or not a regular file (E516).
	ErrLink = errors.New("bundle member is a link or not a regular file")
	// ErrChanged: a member is not the file that was verified at Open (another
	// file, or the same file with a different size or modification time) (E516).
	ErrChanged = errors.New("bundle file changed since it was verified")
	// ErrManifest: manifest.json is unreadable or fails validation (E516 for a
	// v1 manifest, E515 when it is not a v1 manifest at all).
	ErrManifest = errors.New("invalid bundle manifest")
)

Sentinels wrapped inside the E515/E516 errors, for errors.Is.

View Source
var (
	// Sources are the producer.source values.
	Sources = []string{"nself", "postgres", "nhost", "hasura-cloud", "supabase", "firebase", "appwrite"}
	// ExemptionKinds are the exemptions[].kind values.
	ExemptionKinds = []string{"table", "user", "object", "metadata"}
	// SourceCountKinds are the source_counts[].kind values.
	SourceCountKinds = []string{"table", "users", "objects"}
)

Enumerations of the contract, used by the generated schema.

View Source
var DefaultLimits = Limits{
	MaxManifestBytes: 64 << 20,
	MaxFiles:         1_000_000,
	MaxFileBytes:     1 << 40,
	MaxTotalBytes:    8 << 40,
}

DefaultLimits are generous for real exports and small enough that a forged manifest cannot make a reader allocate or hash without bound. Reading is always streamed and capped at the declared length, so a member can never inflate past what the manifest declares.

View Source
var ErrUnsafePath = errors.New("unsafe bundle member path")

ErrUnsafePath is wrapped by CheckMember failures.

Functions

func CheckMember

func CheckMember(rel string) error

CheckMember reports whether rel is a safe member path: relative, slash separated, made only of plain segments.

Purpose: a bundle is untrusted. A listed path must never leave the bundle directory on any platform, so the check is the strictest common subset. Refused: empty or over-long names; invalid UTF-8; control characters and NUL; a backslash (Windows separator); a leading slash; empty, "." and ".." segments (so no "../" and no "//"); ":" in any segment (Windows drive letters "C:", alternate data streams); a segment ending in "." or space; Windows device names (CON, NUL, COM1, LPT1 and the like, with any extension); a name that is not in Unicode NFC (so one name has one spelling, whatever the file system normalises). Outputs: nil, or an error wrapping ErrUnsafePath that says why (never more than the name, which is not a secret).

func MarshalManifest

func MarshalManifest(m Manifest) ([]byte, error)

MarshalManifest returns the canonical manifest.json bytes of m (m should already be sorted).

func StorageMember

func StorageMember(bucket, key string) (string, error)

StorageMember returns the member path of an object's bytes: storage/objects/<hex sha256(bucket + "\x00" + key)>.

Purpose: object keys are arbitrary and case- and Unicode-sensitive; file systems are not. Naming the file by a digest gives every (bucket, key) pair its own member on every platform, so keys like Readme.md and README.md, or the NFC and NFD spellings of one name, export on a case-insensitive or normalising file system as they do on S3. The real bucket and key live in the manifest (storage.objects[]); a reader maps key to member with this function and never parses the file name. Outputs: the member path, or an error wrapping ErrUnsafePath when the bucket is empty or holds NUL (it would make the digest input ambiguous), or the key is empty or not UTF-8.

Types

type Auth

type Auth struct {
	Users          int            `json:"users"`
	HashAlgorithms map[string]int `json:"hash_algorithms"`
	ResetRequired  []string       `json:"reset_required"`
}

Auth summarises auth/users.jsonl.

type Bucket

type Bucket struct {
	Name    string `json:"name"`
	Objects int    `json:"objects"`
	Bytes   int64  `json:"bytes"`
}

Bucket is one storage bucket.

type DB

type DB struct {
	Schemas []string `json:"schemas"`
	Tables  []Table  `json:"tables"`
}

DB lists the dumped schemas and a count and hash per table.

type Exemption

type Exemption struct {
	// Kind is table, user, object or metadata.
	Kind   string `json:"kind"`
	ID     string `json:"id"`
	Reason string `json:"reason"`
}

Exemption records one item deliberately left out, with the reason.

type File

type File struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256"`
	Bytes  int64  `json:"bytes"`
}

File is one bundle member: path relative to the bundle root, with the SHA-256 (lowercase hex) and length of its exact bytes. manifest.json is not listed.

type Hasura

type Hasura struct {
	Tables        int           `json:"tables"`
	Relationships int           `json:"relationships"`
	Permissions   int           `json:"permissions"`
	Functions     int           `json:"functions"`
	Skipped       HasuraSkipped `json:"skipped"`
}

Hasura counts the exported metadata and what was skipped.

type HasuraSkipped

type HasuraSkipped struct {
	RemoteSchemas int `json:"remote_schemas"`
	Actions       int `json:"actions"`
}

HasuraSkipped counts metadata kinds the bundle does not carry.

type Limits

type Limits struct {
	// MaxManifestBytes caps manifest.json.
	MaxManifestBytes int64
	// MaxFiles caps the number of listed members (and of directory entries).
	MaxFiles int
	// MaxFileBytes caps one member's declared length.
	MaxFileBytes int64
	// MaxTotalBytes caps the sum of declared member lengths.
	MaxTotalBytes int64
}

Limits bounds what Open accepts from an untrusted bundle. A zero field takes its DefaultLimits value.

type Manifest

type Manifest struct {
	Format        string        `json:"_format"`
	SchemaVersion string        `json:"schema_version"`
	CreatedAt     string        `json:"created_at"`
	Producer      Producer      `json:"producer"`
	Postgres      PostgresInfo  `json:"postgres"`
	DB            DB            `json:"db"`
	Auth          Auth          `json:"auth"`
	Storage       Storage       `json:"storage"`
	Hasura        Hasura        `json:"hasura"`
	Exemptions    []Exemption   `json:"exemptions"`
	SourceCounts  []SourceCount `json:"source_counts"`
	Compat        []string      `json:"compat"`
	Files         []File        `json:"files"`
}

Manifest is manifest.json, contract:cli.portable-export v1. The JSON schema schemas/portable-export.v1.schema.json is generated from these types by tools/schemagen.

Purpose: one definition of a bundle shared by the importer, the exporter and every source. Constraints: no field may carry a secret value (config lists key names only, in config/keys.txt). Arrays are sorted by Sort; maps marshal in key order. Only CreatedAt is time-dependent.

func (*Manifest) Sort

func (m *Manifest) Sort()

Sort puts every array into its canonical order and replaces nil slices by empty ones, so the marshalled manifest is the same for equal content.

Purpose: Constitution 4.4 deterministic output (sorted keys and arrays). Constraints: PK keeps the key's column order (it is data, not a set); maps need no work because encoding/json marshals them in key order.

func (*Manifest) Validate

func (m *Manifest) Validate() error

Validate checks the manifest against the v1 rules that the generated JSON schema cannot express alone: marker and version, RFC 3339 created_at, lowercase hex SHA-256 values, canonical table hashes, non-negative counts, unique tables, safe NFC member paths that are unique under NFC and case folding (so two members never land on one file of a case-insensitive or normalising file system), and storage objects that each name their own listed file with the same SHA-256 and length, with (bucket, key) unique and no file under storage/objects/ left without an object.

type Object

type Object struct {
	Bucket string `json:"bucket"`
	Key    string `json:"key"`
	Member string `json:"member"`
	SHA256 string `json:"sha256"`
	Bytes  int64  `json:"bytes"`
}

Object is one stored object. Member is StorageMember(Bucket, Key), the bundle file that holds its bytes; SHA256 and Bytes equal that file's entry in files[]. (Bucket, Key) is unique and compared byte for byte.

func NewObject

func NewObject(bucket, key string, f File) (Object, error)

NewObject returns the manifest entry for an object whose bytes were written as member f (the File that WriteFile returned for StorageMember(bucket, key)).

type PostgresInfo

type PostgresInfo struct {
	ServerVersion string `json:"server_version"`
	Major         int    `json:"major"`
}

PostgresInfo describes the server the data was read from.

type Producer

type Producer struct {
	Tool    string `json:"tool"`
	Version string `json:"version"`
	// Source is nself, postgres, nhost, hasura-cloud, supabase, firebase or appwrite.
	Source string `json:"source"`
}

Producer names the tool that wrote the bundle and the kind of source.

type Reader

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

Reader is an opened, fully verified bundle.

func Open

func Open(dir string) (*Reader, error)

Open reads and verifies the bundle in dir with DefaultLimits.

func OpenWithLimits

func OpenWithLimits(dir string, lim Limits) (*Reader, error)

OpenWithLimits is Open with explicit limits.

Purpose: the bundle is untrusted input. Open refuses it unless every check passes, in this order: manifest.json is a regular file within the size limit; it is a v1 portable export (unknown format or major: E515); it parses and validates (E516); member count and sizes are within the limits; every member path is safe and unique; no entry exists that the manifest does not list; every member is a regular, singly linked file reached without a symlink, with the declared length and SHA-256 (E516, naming the file). Outputs: a Reader whose Manifest and members are verified at open time. Constraints: never prints bundle content; messages name paths only.

func (*Reader) Dir

func (r *Reader) Dir() string

Dir returns the bundle directory (symlinks in the caller's path resolved).

func (*Reader) Manifest

func (r *Reader) Manifest() Manifest

Manifest returns the verified manifest.

func (*Reader) Open

func (r *Reader) Open(rel string) (io.ReadCloser, error)

Open returns a reader for a listed member. It opens through OpenFile's checks and re-checks length and SHA-256 as it streams: reading to EOF yields E516 instead of io.EOF if the bytes changed since Open.

func (*Reader) OpenFile

func (r *Reader) OpenFile(rel string) (*os.File, error)

OpenFile opens a listed member and returns the descriptor, for tools that need a file rather than a stream (pg_restore reads standard input, or a /dev/fd path of this descriptor).

Purpose: a path handed to another process can be swapped between the check and the use. This returns an open file instead, after re-checking that the member is still the file that was verified at Open: no symlink on the way (O_NOFOLLOW on the last component where the platform has it), one hard link, and the same device and inode (Windows: volume and file index), size and modification time as the snapshot taken during verification. Any difference is refused with E516 (ErrChanged, or ErrLink for a link). Constraints: the bytes are not hashed again; Open streams them with the length and SHA-256 check. Callers must close the file.

func (*Reader) Verify

func (r *Reader) Verify() error

Verify re-runs the member checks (links, identity, length, SHA-256, unlisted entries) against the snapshot taken at Open.

type SourceCount

type SourceCount struct {
	// Kind is table, users or objects.
	Kind   string `json:"kind"`
	ID     string `json:"id"`
	Count  int64  `json:"count"`
	Method string `json:"method"`
}

SourceCount is the source's own count of one kind, with how it was measured.

type Storage

type Storage struct {
	Buckets []Bucket `json:"buckets"`
	Objects []Object `json:"objects"`
}

Storage lists buckets and objects.

type Table

type Table struct {
	Schema string   `json:"schema"`
	Name   string   `json:"name"`
	Rows   int64    `json:"rows"`
	Hash   string   `json:"hash"`
	PK     []string `json:"pk"`
}

Table is one dumped table; (Schema, Name) is unique. Hash is the canonical decimal string from internal/postgres/tablehash; PK keeps the key's column order.

type Writer

type Writer struct {

	// Now supplies created_at when the manifest leaves it empty; tests pin it.
	Now func() time.Time
	// contains filtered or unexported fields
}

Writer builds a bundle directory. It is not safe for concurrent use.

Purpose: one place that creates bundle files with the right modes and records each file's SHA-256 and length while it streams, so the manifest lists exactly the bytes that were written. Constraints: the bundle directory is 0700 and every file 0600 (EPIC D9). Files are created exclusively, so an existing file or a symlink in the way is an error, never followed. Member paths pass CheckMember.

func NewWriter

func NewWriter(dir string) (*Writer, error)

NewWriter creates dir (0700) and returns a Writer for it. The directory must not exist, or must be empty; an existing non-empty directory is refused so a bundle never mixes with earlier content.

func (*Writer) Dir

func (w *Writer) Dir() string

Dir returns the bundle directory.

func (*Writer) Finish

func (w *Writer) Finish(m Manifest) (Manifest, error)

Finish completes m, sorts it, validates it and writes manifest.json (0600). Format and SchemaVersion are set when empty and must be the v1 values otherwise; CreatedAt defaults to Now() in UTC; Files is replaced by the members written through WriteFile. The manifest bytes are canonical: sorted arrays, sorted map keys, two-space indent, no HTML escaping, one trailing newline; the same input gives the same bytes.

func (*Writer) WriteFile

func (w *Writer) WriteFile(rel string, r io.Reader) (File, error)

WriteFile streams r into the member rel and records its SHA-256 and length. rel must pass CheckMember, must not be manifest.json, and must not repeat an earlier member (compared under NFC and case folding) or collide with a member that is a file where this one needs a directory, or the reverse. On a copy error the partial file is removed.

Jump to

Keyboard shortcuts

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