// Command archiveguard is a stdlib-only, cross-platform CLI that combines
// encrypted archiving with cryptographic round-trip verification and
// secure multi-pass shredding into a single safe pipeline:
//
//	seal  -> pack originals into an AES-256-GCM vault
//	      -> extract that vault to a temp dir and SHA-256 compare
//	         every file against its original, byte for byte
//	      -> only if that verification fully passes, and only if
//	         --apply was given, securely overwrite-and-remove the
//	         originals
//
// A source file is never shredded until its archived copy has been
// proven recoverable. See README.txt for the full write-up.
package main

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// ---------------------------------------------------------------------
// Vault wire format (matches sibling tool VaultZip's format exactly, so
// vaults produced by either tool are interchangeable):
//
//	offset  size  field
//	0       6     magic "VLTZ1\n"
//	6       16    salt
//	22      12    AES-GCM nonce
//	34      ...   AES-256-GCM ciphertext of gzip(tar(files))
//
// Key derivation: 200,000 rounds of iterated SHA-256 over
// (password + salt). This is a dependency-free stand-in for PBKDF2 so
// the tool can build with plain `go build` and no external modules. It
// has NOT been independently audited — same disclosure VaultZip makes
// about its own key stretching. For real-world secret storage, use an
// audited implementation (e.g. golang.org/x/crypto/pbkdf2 or argon2).
// ---------------------------------------------------------------------

const (
	magicHeader = "VLTZ1\n"
	saltSize    = 16
	nonceSize   = 12
	kdfRounds   = 200000
	shredPasses = 3
)

func deriveKey(password string, salt []byte) []byte {
	h := sha256.New()
	h.Write([]byte(password))
	h.Write(salt)
	sum := h.Sum(nil)
	for i := 0; i < kdfRounds; i++ {
		h.Reset()
		h.Write(sum)
		h.Write(salt)
		sum = h.Sum(nil)
	}
	return sum
}

// ---------------------------------------------------------------------
// flag reordering (matches the convention shared across this suite)
// ---------------------------------------------------------------------

func reorderFlags(args []string, valueFlags map[string]bool) []string {
	var flags, positional []string
	for i := 0; i < len(args); i++ {
		a := args[i]
		name := strings.TrimLeft(a, "-")
		if strings.HasPrefix(a, "-") && valueFlags[name] {
			flags = append(flags, a)
			if i+1 < len(args) {
				i++
				flags = append(flags, args[i])
			}
			continue
		}
		if strings.HasPrefix(a, "-") {
			flags = append(flags, a)
			continue
		}
		positional = append(positional, a)
	}
	return append(flags, positional...)
}

// ---------------------------------------------------------------------
// usage
// ---------------------------------------------------------------------

func usage() {
	fmt.Fprint(os.Stderr, `archiveguard - archive, verify, and safely shred (Pro tier: seal->verify->shred)

Usage:
  archiveguard seal <path> [<path> ...] -o <vault.vz> --password <PASS> [--apply]
  archiveguard open <vault.vz> -o <outdir> --password <PASS>
  archiveguard verify <vault.vz> --password <PASS>
  archiveguard help

Commands:
  seal    Pack path(s) into an encrypted vault, verify the vault round-trips
          byte-identically against the originals, and (only with --apply)
          securely erase the original source files. Without --apply, the
          vault is still created and still genuinely verified, but the
          originals are left untouched (dry run of the shred step only).
  open    Decrypt and extract a vault to an output directory.
  verify  Authenticate a vault and list its contents with per-file SHA-256,
          without extracting anything to a permanent location.

Flags:
  -o, --output   <path>   seal: vault file to write. open: directory to extract into.
      --password <pass>   password for encryption/decryption (required)
      --apply              seal: actually shred originals after verified success

Vault format matches the sibling VaultZip tool: magic "VLTZ1\n" + 16-byte
salt + 12-byte GCM nonce + AES-256-GCM ciphertext of gzip(tar(files)).
`)
}

func main() {
	if len(os.Args) < 2 {
		// Double-clicked in Explorer rather than run from a prompt: ask the
		// questions the program needs and stay on screen. Printing usage and
		// exiting here is what made the window vanish instantly.
		if interactiveConsole() {
			runGuided()
			return
		}
		usage()
		os.Exit(1)
	}
	cmd := os.Args[1]
	rest := os.Args[2:]
	switch cmd {
	case "-h", "--help", "help":
		usage()
		return
	case "seal":
		sealCmd(rest)
	case "open":
		openCmd(rest)
	case "verify":
		verifyCmd(rest)
	default:
		fmt.Fprintf(os.Stderr, "archiveguard: unknown command %q\n\n", cmd)
		usage()
		os.Exit(1)
	}
}

// ---------------------------------------------------------------------
// file collection
// ---------------------------------------------------------------------

type fileEntry struct {
	SourcePath  string // real path on disk
	ArchiveName string // slash-separated path stored inside the tar
}

// collectFiles walks each given input path and returns every regular
// file beneath it. A file's ArchiveName is rooted at the basename of
// the input path it came from, so directory structure is preserved on
// extraction.
func collectFiles(paths []string) ([]fileEntry, error) {
	var entries []fileEntry
	seen := map[string]bool{}
	for _, p := range paths {
		info, err := os.Lstat(p)
		if err != nil {
			return nil, fmt.Errorf("cannot access %q: %w", p, err)
		}
		base := filepath.Base(filepath.Clean(p))
		if info.IsDir() {
			root := filepath.Clean(p)
			err := filepath.Walk(root, func(walkPath string, wi os.FileInfo, werr error) error {
				if werr != nil {
					return werr
				}
				if wi.IsDir() {
					return nil
				}
				if !wi.Mode().IsRegular() {
					return nil
				}
				rel, rerr := filepath.Rel(root, walkPath)
				if rerr != nil {
					return rerr
				}
				archiveName := filepath.ToSlash(filepath.Join(base, rel))
				if seen[archiveName] {
					return fmt.Errorf("duplicate archive path %q", archiveName)
				}
				seen[archiveName] = true
				entries = append(entries, fileEntry{SourcePath: walkPath, ArchiveName: archiveName})
				return nil
			})
			if err != nil {
				return nil, err
			}
		} else if info.Mode().IsRegular() {
			if seen[base] {
				return nil, fmt.Errorf("duplicate archive path %q", base)
			}
			seen[base] = true
			entries = append(entries, fileEntry{SourcePath: p, ArchiveName: base})
		} else {
			return nil, fmt.Errorf("%q is neither a regular file nor a directory", p)
		}
	}
	if len(entries) == 0 {
		return nil, errors.New("no regular files found under the given path(s)")
	}
	sort.Slice(entries, func(i, j int) bool { return entries[i].ArchiveName < entries[j].ArchiveName })
	return entries, nil
}

// ---------------------------------------------------------------------
// packing / encryption
// ---------------------------------------------------------------------

func buildTarGz(entries []fileEntry) ([]byte, error) {
	var tarBuf bytes.Buffer
	tw := tar.NewWriter(&tarBuf)
	for _, e := range entries {
		f, err := os.Open(e.SourcePath)
		if err != nil {
			return nil, fmt.Errorf("reading %q: %w", e.SourcePath, err)
		}
		info, err := f.Stat()
		if err != nil {
			f.Close()
			return nil, err
		}
		hdr, err := tar.FileInfoHeader(info, "")
		if err != nil {
			f.Close()
			return nil, err
		}
		hdr.Name = e.ArchiveName
		if err := tw.WriteHeader(hdr); err != nil {
			f.Close()
			return nil, err
		}
		if _, err := io.Copy(tw, f); err != nil {
			f.Close()
			return nil, err
		}
		f.Close()
	}
	if err := tw.Close(); err != nil {
		return nil, err
	}

	var gzBuf bytes.Buffer
	gw := gzip.NewWriter(&gzBuf)
	if _, err := gw.Write(tarBuf.Bytes()); err != nil {
		return nil, err
	}
	if err := gw.Close(); err != nil {
		return nil, err
	}
	return gzBuf.Bytes(), nil
}

func encryptVault(plaintext []byte, password string) ([]byte, error) {
	salt := make([]byte, saltSize)
	if _, err := rand.Read(salt); err != nil {
		return nil, err
	}
	nonce := make([]byte, nonceSize)
	if _, err := rand.Read(nonce); err != nil {
		return nil, err
	}
	key := deriveKey(password, salt)
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	ciphertext := gcm.Seal(nil, nonce, plaintext, nil)

	var out bytes.Buffer
	out.WriteString(magicHeader)
	out.Write(salt)
	out.Write(nonce)
	out.Write(ciphertext)
	return out.Bytes(), nil
}

// decryptVault reads a vault file from disk and returns the decrypted
// gzip(tar(...)) plaintext. Errors clearly on bad magic, truncation,
// or authentication failure (wrong password / corrupted vault).
func decryptVault(vaultPath, password string) ([]byte, error) {
	raw, err := os.ReadFile(vaultPath)
	if err != nil {
		return nil, fmt.Errorf("reading vault: %w", err)
	}
	minLen := len(magicHeader) + saltSize + nonceSize
	if len(raw) < minLen {
		return nil, fmt.Errorf("not a valid archiveguard vault (too short)")
	}
	if string(raw[:len(magicHeader)]) != magicHeader {
		return nil, fmt.Errorf("not a valid archiveguard vault (bad magic header)")
	}
	off := len(magicHeader)
	salt := raw[off : off+saltSize]
	off += saltSize
	nonce := raw[off : off+nonceSize]
	off += nonceSize
	ciphertext := raw[off:]

	key := deriveKey(password, salt)
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, fmt.Errorf("authentication failed: wrong password or corrupted vault")
	}
	return plaintext, nil
}

type vaultFile struct {
	Name string
	Data []byte
	Mode os.FileMode
}

func untarGz(gzData []byte) ([]vaultFile, error) {
	gr, err := gzip.NewReader(bytes.NewReader(gzData))
	if err != nil {
		return nil, fmt.Errorf("gzip decode: %w", err)
	}
	defer gr.Close()
	tr := tar.NewReader(gr)
	var files []vaultFile
	for {
		hdr, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("tar decode: %w", err)
		}
		if hdr.Typeflag != tar.TypeReg {
			continue
		}
		name := filepath.ToSlash(hdr.Name)
		if strings.Contains(name, "..") || strings.HasPrefix(name, "/") {
			return nil, fmt.Errorf("refusing unsafe path in vault: %q", name)
		}
		data, err := io.ReadAll(tr)
		if err != nil {
			return nil, err
		}
		mode := hdr.FileInfo().Mode()
		if mode == 0 {
			mode = 0o644
		}
		files = append(files, vaultFile{Name: name, Data: data, Mode: mode})
	}
	return files, nil
}

func sha256Hex(data []byte) string {
	sum := sha256.Sum256(data)
	return hex.EncodeToString(sum[:])
}

func sha256File(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return "", err
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}

// ---------------------------------------------------------------------
// secure shred (same mechanism as sibling tool PrivacySweep: multi-pass
// random overwrite + fsync, then remove)
// ---------------------------------------------------------------------

func shredFile(path string) error {
	// Ensure the file is writable before we try to overwrite it.
	if info, err := os.Stat(path); err == nil {
		_ = os.Chmod(path, info.Mode()|0o600)
	}
	f, err := os.OpenFile(path, os.O_WRONLY, 0)
	if err != nil {
		return fmt.Errorf("opening for shred: %w", err)
	}
	info, err := f.Stat()
	if err != nil {
		f.Close()
		return err
	}
	size := info.Size()
	buf := make([]byte, 64*1024)
	for pass := 0; pass < shredPasses; pass++ {
		if _, err := f.Seek(0, io.SeekStart); err != nil {
			f.Close()
			return err
		}
		remaining := size
		for remaining > 0 {
			n := int64(len(buf))
			if remaining < n {
				n = remaining
			}
			if _, err := rand.Read(buf[:n]); err != nil {
				f.Close()
				return err
			}
			if _, err := f.Write(buf[:n]); err != nil {
				f.Close()
				return err
			}
			remaining -= n
		}
		if err := f.Sync(); err != nil {
			f.Close()
			return err
		}
	}
	if err := f.Truncate(0); err != nil {
		f.Close()
		return err
	}
	if err := f.Sync(); err != nil {
		f.Close()
		return err
	}
	if err := f.Close(); err != nil {
		return err
	}
	return os.Remove(path)
}

// ---------------------------------------------------------------------
// seal
// ---------------------------------------------------------------------

func sealCmd(args []string) {
	valueFlags := map[string]bool{"o": true, "output": true, "password": true}
	args = reorderFlags(args, valueFlags)

	var output, password string
	var apply bool
	var positional []string

	for i := 0; i < len(args); i++ {
		a := args[i]
		switch a {
		case "-h", "--help", "help":
			usage()
			return
		case "-o", "--output":
			i++
			if i >= len(args) {
				fmt.Fprintln(os.Stderr, "archiveguard seal: -o/--output requires a value")
				os.Exit(1)
			}
			output = args[i]
		case "--password":
			i++
			if i >= len(args) {
				fmt.Fprintln(os.Stderr, "archiveguard seal: --password requires a value")
				os.Exit(1)
			}
			password = args[i]
		case "--apply":
			apply = true
		default:
			if strings.HasPrefix(a, "-") {
				fmt.Fprintf(os.Stderr, "archiveguard seal: unknown flag %q\n", a)
				os.Exit(1)
			}
			positional = append(positional, a)
		}
	}

	if len(positional) == 0 {
		fmt.Fprintln(os.Stderr, "archiveguard seal: at least one <path> is required")
		usage()
		os.Exit(1)
	}
	if output == "" {
		fmt.Fprintln(os.Stderr, "archiveguard seal: -o/--output is required")
		os.Exit(1)
	}
	if password == "" {
		fmt.Fprintln(os.Stderr, "archiveguard seal: --password is required")
		os.Exit(1)
	}

	entries, err := collectFiles(positional)
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard seal: %v\n", err)
		os.Exit(1)
	}

	// Precompute original hashes before touching anything, so a later
	// bug can never make us compare a file against itself post-shred.
	origHashes := make(map[string]string, len(entries))
	for _, e := range entries {
		h, err := sha256File(e.SourcePath)
		if err != nil {
			fmt.Fprintf(os.Stderr, "archiveguard seal: hashing %q: %v\n", e.SourcePath, err)
			os.Exit(1)
		}
		origHashes[e.ArchiveName] = h
	}

	plaintext, err := buildTarGz(entries)
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard seal: packing failed: %v\n", err)
		os.Exit(1)
	}
	vaultBytes, err := encryptVault(plaintext, password)
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard seal: encryption failed: %v\n", err)
		os.Exit(1)
	}

	// Write to a temp file first, then rename into place, so a failure
	// partway through never leaves a half-written vault at the final path.
	outDir := filepath.Dir(output)
	if outDir == "" {
		outDir = "."
	}
	tmpOut, err := os.CreateTemp(outDir, ".archiveguard-vault-*.tmp")
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard seal: %v\n", err)
		os.Exit(1)
	}
	tmpOutPath := tmpOut.Name()
	if _, err := tmpOut.Write(vaultBytes); err != nil {
		tmpOut.Close()
		os.Remove(tmpOutPath)
		fmt.Fprintf(os.Stderr, "archiveguard seal: writing vault: %v\n", err)
		os.Exit(1)
	}
	if err := tmpOut.Sync(); err != nil {
		tmpOut.Close()
		os.Remove(tmpOutPath)
		fmt.Fprintf(os.Stderr, "archiveguard seal: writing vault: %v\n", err)
		os.Exit(1)
	}
	if err := tmpOut.Close(); err != nil {
		os.Remove(tmpOutPath)
		fmt.Fprintf(os.Stderr, "archiveguard seal: writing vault: %v\n", err)
		os.Exit(1)
	}
	if err := os.Rename(tmpOutPath, output); err != nil {
		os.Remove(tmpOutPath)
		fmt.Fprintf(os.Stderr, "archiveguard seal: finalizing vault: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("sealed %d file(s) into %s (%d bytes)\n", len(entries), output, len(vaultBytes))

	// -----------------------------------------------------------------
	// DEBUG/TEST-ONLY HOOK (added deliberately for this suite's required
	// safety test 5, matching the pattern used by sibling tools in this
	// batch): if the environment variable ARCHIVEGUARD_DEBUG_CORRUPT_VAULT
	// is set to "1", flip one byte of the just-written vault file here,
	// AFTER it has been written but BEFORE round-trip verification reads
	// it back. This exists purely to prove that a genuine verification
	// failure aborts the pipeline and leaves originals untouched. It is
	// inert by default (unset in normal use and in the shipped
	// cross-compiled binaries) and does nothing unless that exact
	// environment variable is explicitly set to "1".
	if os.Getenv("ARCHIVEGUARD_DEBUG_CORRUPT_VAULT") == "1" {
		if err := debugCorruptFile(output); err != nil {
			fmt.Fprintf(os.Stderr, "archiveguard seal: debug corruption hook failed: %v\n", err)
			os.Exit(1)
		}
		fmt.Fprintln(os.Stderr, "[debug] ARCHIVEGUARD_DEBUG_CORRUPT_VAULT=1: corrupted one byte of the vault for testing")
	}
	// -----------------------------------------------------------------

	// Verify: decrypt+extract the just-written vault into a temp dir and
	// byte-compare every extracted file against its original source.
	tmpDir, err := os.MkdirTemp("", "archiveguard-verify-*")
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard seal: verification setup failed: %v\n", err)
		os.Exit(1)
	}

	verifyErr := verifyRoundTrip(output, password, tmpDir, entries, origHashes)
	// The temp verification directory must be removed on every exit path
	// from here on, including os.Exit below (which skips defers), so we
	// remove it explicitly rather than relying on defer.
	os.RemoveAll(tmpDir)

	if verifyErr != nil {
		fmt.Fprintln(os.Stderr, "VERIFICATION FAILED - aborting. No source files were touched.")
		fmt.Fprintf(os.Stderr, "reason: %v\n", verifyErr)
		fmt.Fprintf(os.Stderr, "the vault at %s may be incomplete or corrupted; the original files are untouched and safe.\n", output)
		os.Exit(1)
	}

	fmt.Printf("verification passed: all %d file(s) round-trip byte-identical\n", len(entries))

	if !apply {
		fmt.Printf("dry run (no --apply): would now securely shred %d original file(s) - re-run with --apply to actually do that\n", len(entries))
		return
	}

	fmt.Println("shredding originals (multi-pass overwrite + fsync)...")
	var shredded []string
	for _, e := range entries {
		if err := shredFile(e.SourcePath); err != nil {
			fmt.Fprintf(os.Stderr, "archiveguard seal: WARNING: failed to shred %q: %v\n", e.SourcePath, err)
			continue
		}
		shredded = append(shredded, e.SourcePath)
	}
	fmt.Printf("shredded %d/%d original file(s):\n", len(shredded), len(entries))
	for _, s := range shredded {
		fmt.Printf("  %s\n", s)
	}
}

// verifyRoundTrip decrypts vaultPath, extracts it into tmpDir, and
// SHA-256 compares every extracted file against origHashes. It also
// confirms every expected entry was actually present in the vault.
func verifyRoundTrip(vaultPath, password, tmpDir string, entries []fileEntry, origHashes map[string]string) error {
	plaintext, err := decryptVault(vaultPath, password)
	if err != nil {
		return err
	}
	files, err := untarGz(plaintext)
	if err != nil {
		return fmt.Errorf("extracting vault contents: %w", err)
	}

	extractedHashes := make(map[string]string, len(files))
	for _, vf := range files {
		destPath := filepath.Join(tmpDir, filepath.FromSlash(vf.Name))
		if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
			return fmt.Errorf("preparing verification dir: %w", err)
		}
		if err := os.WriteFile(destPath, vf.Data, vf.Mode.Perm()|0o600); err != nil {
			return fmt.Errorf("writing verification copy of %q: %w", vf.Name, err)
		}
		extractedHashes[vf.Name] = sha256Hex(vf.Data)
	}

	var mismatches []string
	for _, e := range entries {
		gotHash, ok := extractedHashes[e.ArchiveName]
		if !ok {
			mismatches = append(mismatches, fmt.Sprintf("%s: missing from vault", e.ArchiveName))
			continue
		}
		wantHash := origHashes[e.ArchiveName]
		if gotHash != wantHash {
			mismatches = append(mismatches, fmt.Sprintf("%s: hash mismatch (original %s, extracted %s)", e.ArchiveName, wantHash, gotHash))
		}
	}
	if len(extractedHashes) != len(entries) {
		mismatches = append(mismatches, fmt.Sprintf("vault contains %d file(s), expected %d", len(extractedHashes), len(entries)))
	}
	if len(mismatches) > 0 {
		return fmt.Errorf("%d mismatch(es):\n  %s", len(mismatches), strings.Join(mismatches, "\n  "))
	}
	return nil
}

// debugCorruptFile flips one byte deep in the ciphertext region of a
// vault file on disk. Test-only, see the call site above.
func debugCorruptFile(path string) error {
	f, err := os.OpenFile(path, os.O_RDWR, 0)
	if err != nil {
		return err
	}
	defer f.Close()
	info, err := f.Stat()
	if err != nil {
		return err
	}
	headerLen := int64(len(magicHeader) + saltSize + nonceSize)
	if info.Size() <= headerLen+1 {
		return fmt.Errorf("vault too small to corrupt")
	}
	offset := headerLen + 1
	b := make([]byte, 1)
	if _, err := f.ReadAt(b, offset); err != nil {
		return err
	}
	b[0] ^= 0xFF
	_, err = f.WriteAt(b, offset)
	return err
}

// ---------------------------------------------------------------------
// open
// ---------------------------------------------------------------------

func openCmd(args []string) {
	valueFlags := map[string]bool{"o": true, "output": true, "password": true}
	args = reorderFlags(args, valueFlags)

	var output, password string
	var positional []string

	for i := 0; i < len(args); i++ {
		a := args[i]
		switch a {
		case "-h", "--help", "help":
			usage()
			return
		case "-o", "--output":
			i++
			if i >= len(args) {
				fmt.Fprintln(os.Stderr, "archiveguard open: -o/--output requires a value")
				os.Exit(1)
			}
			output = args[i]
		case "--password":
			i++
			if i >= len(args) {
				fmt.Fprintln(os.Stderr, "archiveguard open: --password requires a value")
				os.Exit(1)
			}
			password = args[i]
		default:
			if strings.HasPrefix(a, "-") {
				fmt.Fprintf(os.Stderr, "archiveguard open: unknown flag %q\n", a)
				os.Exit(1)
			}
			positional = append(positional, a)
		}
	}

	if len(positional) != 1 {
		fmt.Fprintln(os.Stderr, "archiveguard open: exactly one <vault.vz> is required")
		usage()
		os.Exit(1)
	}
	if output == "" {
		fmt.Fprintln(os.Stderr, "archiveguard open: -o/--output is required")
		os.Exit(1)
	}
	if password == "" {
		fmt.Fprintln(os.Stderr, "archiveguard open: --password is required")
		os.Exit(1)
	}
	vaultPath := positional[0]

	plaintext, err := decryptVault(vaultPath, password)
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard open: %v\n", err)
		os.Exit(1)
	}
	files, err := untarGz(plaintext)
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard open: %v\n", err)
		os.Exit(1)
	}
	if err := os.MkdirAll(output, 0o755); err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard open: %v\n", err)
		os.Exit(1)
	}
	for _, vf := range files {
		destPath := filepath.Join(output, filepath.FromSlash(vf.Name))
		if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
			fmt.Fprintf(os.Stderr, "archiveguard open: %v\n", err)
			os.Exit(1)
		}
		if err := os.WriteFile(destPath, vf.Data, vf.Mode.Perm()|0o600); err != nil {
			fmt.Fprintf(os.Stderr, "archiveguard open: %v\n", err)
			os.Exit(1)
		}
	}
	fmt.Printf("extracted %d file(s) to %s\n", len(files), output)
}

// ---------------------------------------------------------------------
// verify
// ---------------------------------------------------------------------

func verifyCmd(args []string) {
	valueFlags := map[string]bool{"password": true}
	args = reorderFlags(args, valueFlags)

	var password string
	var positional []string

	for i := 0; i < len(args); i++ {
		a := args[i]
		switch a {
		case "-h", "--help", "help":
			usage()
			return
		case "--password":
			i++
			if i >= len(args) {
				fmt.Fprintln(os.Stderr, "archiveguard verify: --password requires a value")
				os.Exit(1)
			}
			password = args[i]
		default:
			if strings.HasPrefix(a, "-") {
				fmt.Fprintf(os.Stderr, "archiveguard verify: unknown flag %q\n", a)
				os.Exit(1)
			}
			positional = append(positional, a)
		}
	}

	if len(positional) != 1 {
		fmt.Fprintln(os.Stderr, "archiveguard verify: exactly one <vault.vz> is required")
		usage()
		os.Exit(1)
	}
	if password == "" {
		fmt.Fprintln(os.Stderr, "archiveguard verify: --password is required")
		os.Exit(1)
	}
	vaultPath := positional[0]

	plaintext, err := decryptVault(vaultPath, password)
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard verify: %v\n", err)
		os.Exit(1)
	}
	files, err := untarGz(plaintext)
	if err != nil {
		fmt.Fprintf(os.Stderr, "archiveguard verify: %v\n", err)
		os.Exit(1)
	}
	sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
	fmt.Printf("vault OK: %d file(s), password authenticated\n", len(files))
	for _, vf := range files {
		fmt.Printf("  %-12s %8d bytes  %s\n", sha256Hex(vf.Data)[:12], len(vf.Data), vf.Name)
	}
}
