treeport

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 12 Imported by: 0

README

Treeport

한국어 · Profiles · JSON/API contract · Measured comparison

Find the file names that will break on another operating system before you ship a ZIP, export a folder, or someone clones your repository on Windows. Treeport reads a Git index, a folder, a ZIP or a JSONL list, and reports names that collide or are invalid on the destination you name: README.md next to Readme.md, aux.c, a trailing dot, a path over 260 characters, or a Korean name written in decomposed Unicode on a Mac. It never renames, copies, or extracts files.

CAFÉ/a?b.txt
café/a*b.txt
       ↓ export-fold: NFC → case fold → replace → trim → truncate
café/a_b.txt     collision: normalization + case + replacement

The two parents also converge, even when their children have different names. Reports include those implicit parent conflicts, duplicate entries, file/directory obstructions, and exact original bytes.

Choose a destination model explicitly: posix, windows, macos, or the precisely specified export-fold transformation pipeline. Results are known-compatible, incompatible, or unknown. Where exact Windows or macOS tables are unavailable for non-ASCII names, the result is unknown with a hint about what the destination most likely does.

Install and run

Download a binary and SHA-256 checksums from Releases, or build with Go 1.25+:

go install github.com/rad1092/treeport/cmd/treeport@v0.2.0
treeport version
treeport git --profile windows --root 'C:\src\project'      # this repository, as a Windows clone
treeport scan --profile windows --root 'C:\export\release' ./dist
treeport zip --profile windows --root 'C:\export\release' --json release.zip

treeport git reads git ls-files, so it checks the names a clone creates, not whatever happens to be in your working tree. It is also available as a pre-commit hook and a GitHub Action:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/rad1092/treeport
    rev: v0.2.0
    hooks:
      - id: treeport

# .github/workflows/names.yml, after actions/checkout
      - uses: rad1092/treeport@v0.2.0
        with:
          profile: windows
          root: 'C:\src\project'

All flags precede the input path. --root means the future destination root, not the input directory. It must be an absolute drive or UNC path for windows; it need not exist on the machine running Treeport.

A manifest can represent a tree the current filesystem cannot store:

printf '%s\n' '{"path":"CAFÉ/a?b.txt"}' '{"path":"cafe\u0301/a*b.txt"}' |
  treeport manifest --profile export-fold --json -

This exits 1 and reports both the parent alias and the full-path collision. Use --max-component and --max-path to match an export pipeline's explicit budgets; profile defaults and units are documented.

For a known-good first check, replace the manifest with {"path":"reports/week-42.csv"} and use the same command: expect exit 0 and status: "known-compatible". A complete first-run example covers passing and failing reports, Windows paths, and common installation errors. The public Go API is already indexed on pkg.go.dev.

Exit Meaning
0 Known compatible with the selected model, or unknown but very likely compatible
1 At least one definite incompatibility, or unknown names that very likely collide
2 Invalid arguments, unreadable/malformed input, limit exceeded, cancellation, or runtime error
3 Unknown and Treeport cannot tell which way (symlinks, invalid UTF-8, truncation)

--strict turns every unknown result into exit 3, ignoring the likely verdict. The JSON status stays unknown either way and likely says which way the hints point. Inspect complete as well: diagnostic truncation is explicitly marked and never becomes a success. CI and pre-commit examples retain JSON reports for diagnosis.

Embed in Go

report, err := treeport.Check(ctx, []treeport.Entry{
    {Path: "café/one.txt", Kind: "file"},
    {Path: "cafe\u0301/two.txt", Kind: "file"},
}, treeport.Options{Profile: "export-fold"})

Check does not mutate the input slice or touch the filesystem. See the runnable example and input adapters. A context cancels cooperative work; resource budgets bound entries, indexed parents, raw path bytes, and diagnostics.

Why a tree check?

Normalization differences have caused rclone filter mismatches and backend path failures. Syncthing documents case conflicts that stop synchronization. These are evidence of repeated cross-system filename problems, not endorsements of Treeport.

Existing tools are useful: pre-commit already checks case conflicts and illegal Windows names; CrossRename supports recursive dry-runs and length handling; Pathologize provides path sanitization. Treeport adds a read-only tree contract around combined transformations, destination root budgets, explained groups, and manifest/archive inputs. The independent corpus and pinned executions show concrete differences and give existing tools credit for what they already catch.

Scope and development

Treeport examines names and tree structure. It does not validate contents, destination permissions, free space, cloud-provider rules, existing destination files, link targets, or races with a changing source. No universal portability or archive-security guarantee is made. Read limitations before using a report as a release gate.

go test ./...
go test -race ./...
go vet ./...
go run ./examples/embed

The maintained corpus covers NFC/NFD, Korean, emoji UTF-16 budgets, fullwidth distinctions, superscript reserved names, invalid UTF-8, implicit parents, duplicates, and traversal. See verification for measured environments, CI scope, and reproducible stress runs. MIT license.

Third-party licenses and pinned provenance accompany source and binary distributions.

Documentation

Overview

Package treeport checks complete destination trees without changing the source.

Index

Constants

View Source
const SchemaVersion = "1"
View Source
const Version = "0.2.0"

Variables

This section is empty.

Functions

This section is empty.

Types

type Conflict

type Conflict struct {
	ID      string   `json:"id"`
	Target  string   `json:"target"`
	Status  string   `json:"status"`
	Causes  []string `json:"causes"`
	Members []Member `json:"members"`
	Hint    string   `json:"hint,omitempty"`
}

type Entry

type Entry struct {
	Path string
	Kind string
}

type Issue

type Issue struct {
	Code   string   `json:"code"`
	Status string   `json:"status"`
	Path   Original `json:"path"`
	Detail string   `json:"detail"`
	// Hint is a practical reading of an unknown result, such as whether two
	// names very likely collide on the destination.
	Hint string `json:"hint,omitempty"`
}

type Limits

type Limits struct {
	MaxEntries    int
	MaxNodes      int
	MaxPathBytes  int
	MaxTotalBytes int64
	MaxIssues     int
	MaxIndexBytes int64
}

Paths are slash-delimited relative raw bytes. Kind: file, directory, symlink.

func DefaultLimits

func DefaultLimits() Limits

func NormalizeLimits

func NormalizeLimits(l Limits) (Limits, error)

NormalizeLimits replaces zero values with defaults and rejects negative limits.

type Member

type Member struct {
	Path      Original `json:"path"`
	Kind      string   `json:"kind"`
	Implicit  bool     `json:"implicit"`
	Count     int      `json:"count"`
	Uncertain bool     `json:"uncertain"`
}

type Options

type Options struct {
	Profile         string
	DestinationRoot string
	MaxComponent    int
	MaxPath         int
	Limits          Limits
}

type Original

type Original struct {
	Display string `json:"display"`
	Base64  string `json:"base64"`
}

func OriginalPath

func OriginalPath(s string) Original

type Report

type Report struct {
	SchemaVersion   string `json:"schema_version"`
	ToolVersion     string `json:"tool_version"`
	Profile         string `json:"profile"`
	ComponentBudget int    `json:"component_budget"`
	PathBudget      int    `json:"path_budget"`
	LengthUnit      string `json:"length_unit"`
	ProfileVersion  string `json:"profile_version"`
	UnicodeVersion  string `json:"unicode_version"`
	DestinationRoot string `json:"destination_root"`
	Status          string `json:"status"`
	// Likely is set when Status is unknown: "compatible" or "incompatible"
	// when the hints point one way, empty when Treeport cannot tell.
	Likely     string  `json:"likely,omitempty"`
	Complete   bool    `json:"complete"`
	EntryCount int     `json:"entry_count"`
	NodeCount  int     `json:"node_count"`
	Issues     []Issue `json:"issues"`
	// Warnings never change Status. nfd_name flags decomposed Unicode names.
	Warnings    []Issue    `json:"warnings"`
	Conflicts   []Conflict `json:"conflicts"`
	Limitations []string   `json:"limitations"`
}

func Check

func Check(ctx context.Context, entries []Entry, opts Options) (report Report, retErr error)

Check evaluates names against an explicit, versioned model. A compatible result applies only to that model, not all mounts, OS APIs, or concurrent filesystem changes. Check never changes entries or reads their file contents. Partial scans return errors.

Directories

Path Synopsis
cmd
treeport command
Command treeport performs read-only, model-scoped destination tree checks.
Command treeport performs read-only, model-scoped destination tree checks.
examples
embed command
Embed the deterministic export model before accepting a generated tree.
Embed the deterministic export model before accepting a generated tree.
Package input reads source names without renaming, copying, following tree symlinks, or decompressing archive members.
Package input reads source names without renaming, copying, following tree symlinks, or decompressing archive members.
scripts
release command
Command release builds reproducible-layout release archives and SHA256SUMS.
Command release builds reproducible-layout release archives and SHA256SUMS.

Jump to

Keyboard shortcuts

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