Import from Quazim0t0/neural-storage; repoint refs to NeuralVerified
Browse files- GF256.pt +3 -0
- LICENSE +21 -0
- README.md +79 -0
- cli.py +93 -0
- requirements.txt +1 -0
- step1_storage.py +73 -0
- step2_rs.py +68 -0
- step3_vault.py +87 -0
- storage/__init__.py +0 -0
- storage/archive.py +120 -0
- storage/chunkstore.py +79 -0
- storage/common.py +56 -0
- storage/diskimage.py +141 -0
- storage/gf256.py +80 -0
- storage/rs.py +104 -0
- storage/scan.py +57 -0
- storage/vault.py +152 -0
- storage/vaultfs.py +129 -0
GF256.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:95a3bd56efcb21b046ccb4b400a0965d720bd4828e883cb543c584f2e9dca77d
|
| 3 |
+
size 564882
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Dean Byrne (Quazim0t0)
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: pytorch
|
| 4 |
+
tags:
|
| 5 |
+
- verified-units
|
| 6 |
+
- reed-solomon
|
| 7 |
+
- erasure-coding
|
| 8 |
+
- deduplication
|
| 9 |
+
- storage
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# neural-storage — self-healing vault with a verified GF(256) core
|
| 13 |
+
|
| 14 |
+
**Repositories:** [GitHub](https://github.com/quzi93/neural-storage) · [🤗 HuggingFace](https://huggingface.co/NeuralVerified/neural-storage)
|
| 15 |
+
|
| 16 |
+
A content-addressed, deduplicating, **self-healing** storage vault. Any *k* of *n*
|
| 17 |
+
Reed-Solomon shards reconstruct the whole, so pieces can be lost or corrupted and
|
| 18 |
+
the data survives. The erasure-coding arithmetic — GF(2⁸) multiply — is provided
|
| 19 |
+
by neural `LOG`/`EXP` units verified **bit-exact over all 65,536 (a,b) pairs**,
|
| 20 |
+
the same N/N discipline as
|
| 21 |
+
[neural-aarch64-units](https://huggingface.co/NeuralVerified/neural-aarch64-units).
|
| 22 |
+
|
| 23 |
+
> **Honest by design:** dedup removes *redundant* data; RS *adds* redundancy for
|
| 24 |
+
> resilience. Incompressible data never shrinks — you cannot beat entropy. Chunk
|
| 25 |
+
> hashes are real SHA-256, not a neural emulation.
|
| 26 |
+
|
| 27 |
+
## What's verified
|
| 28 |
+
|
| 29 |
+
- `GF256` (`LOG`/`EXP`): GF(2⁸) multiply — **composed multiply 65536/65536**
|
| 30 |
+
- Reed-Solomon any-*k*-of-*n* recovery (every loss pattern)
|
| 31 |
+
- `Vault`: dedup + RS shards on disk, survives deleted/corrupted shards, `heal()`s
|
| 32 |
+
- `VaultFS` + optional WinFsp drive-letter mount
|
| 33 |
+
|
| 34 |
+
## Use
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
pip install torch
|
| 38 |
+
python step1_storage.py # verified GF(256) + dedup chunk store
|
| 39 |
+
python step2_rs.py # Reed-Solomon recovery
|
| 40 |
+
python step3_vault.py # self-healing vault
|
| 41 |
+
|
| 42 |
+
python cli.py store <vault> <folder>
|
| 43 |
+
python cli.py export <vault> <folder> # RS-healed reconstruct
|
| 44 |
+
python cli.py mount <vault> X: # requires WinFsp
|
| 45 |
+
|
| 46 |
+
# image a whole drive/partition into a self-healing .pt (may need admin):
|
| 47 |
+
python cli.py image \\.\C: diskC.pt
|
| 48 |
+
python cli.py image-verify diskC.pt
|
| 49 |
+
python cli.py image-restore diskC.pt out.img
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
Weights: `GF256.pt`.
|
| 53 |
+
|
| 54 |
+
**Create your own verified unit** (template: `storage/gf256.py`): write the exact
|
| 55 |
+
golden finite function → enumerate the domain (decompose big/linear ones into
|
| 56 |
+
bit/byte slices, see `storage/rs.py`) → `common.train` → `common.verify` must be
|
| 57 |
+
bit-exact on 100% of inputs → compose. `step1_storage.py` shows the full loop.
|
| 58 |
+
|
| 59 |
+
## Citation
|
| 60 |
+
|
| 61 |
+
```bibtex
|
| 62 |
+
@misc{byrne2026neuralstorage,
|
| 63 |
+
title = {neural-storage: Self-Healing Erasure-Coded Vault with a Verified GF(256) Core},
|
| 64 |
+
author = {Byrne, Dean (Quazim0t0)},
|
| 65 |
+
year = {2026},
|
| 66 |
+
howpublished = {\url{https://huggingface.co/NeuralVerified/neural-storage}}
|
| 67 |
+
}
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
**Dean Byrne (Quazim0t0)** · 2026
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
<!-- neuralverified-relocation-note -->
|
| 76 |
+
> **Now hosted by [NeuralVerified](https://huggingface.co/NeuralVerified).**
|
| 77 |
+
>
|
| 78 |
+
> This repo was moved into the NeuralVerified organization to help organize my profile.
|
| 79 |
+
> Originally published at [`Quazim0t0/neural-storage`](https://huggingface.co/Quazim0t0/neural-storage).
|
cli.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
neural-vault CLI. Dependency-free except `mount` (needs WinFsp + winfspy).
|
| 3 |
+
|
| 4 |
+
store <vault> <src_folder> [--k K --m M] ingest a folder (dedup + RS)
|
| 5 |
+
ls <vault> list stored files
|
| 6 |
+
stat <vault> dedup / redundancy stats
|
| 7 |
+
export <vault> <dst_folder> reconstruct all files (RS-healed)
|
| 8 |
+
heal <vault> regenerate any missing shards
|
| 9 |
+
mount <vault> <mountpoint> expose as a drive (WinFsp)
|
| 10 |
+
|
| 11 |
+
image <device> <out.pt> [--k --m --part-mb] scan a whole drive/partition
|
| 12 |
+
(e.g. \\\\.\\C: or an .img, may need admin) into a self-healing .pt
|
| 13 |
+
image-verify <out.pt> report archive health
|
| 14 |
+
image-heal <out.pt> repair detected bit-rot
|
| 15 |
+
image-restore <out.pt> <out.img> reconstruct the image bit-exact
|
| 16 |
+
"""
|
| 17 |
+
import argparse
|
| 18 |
+
from storage.vault import Vault
|
| 19 |
+
from storage.vaultfs import winfsp_available
|
| 20 |
+
from storage import diskimage
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main():
|
| 24 |
+
ap = argparse.ArgumentParser(prog="neural-vault")
|
| 25 |
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
| 26 |
+
p = sub.add_parser("store"); p.add_argument("vault"); p.add_argument("src")
|
| 27 |
+
p.add_argument("--k", type=int, default=4); p.add_argument("--m", type=int, default=2)
|
| 28 |
+
for name in ("ls", "stat", "heal"):
|
| 29 |
+
q = sub.add_parser(name); q.add_argument("vault")
|
| 30 |
+
e = sub.add_parser("export"); e.add_argument("vault"); e.add_argument("dst")
|
| 31 |
+
mo = sub.add_parser("mount"); mo.add_argument("vault"); mo.add_argument("mountpoint")
|
| 32 |
+
im = sub.add_parser("image"); im.add_argument("device"); im.add_argument("out")
|
| 33 |
+
im.add_argument("--k", type=int, default=4); im.add_argument("--m", type=int, default=2)
|
| 34 |
+
im.add_argument("--part-mb", type=int, default=64); im.add_argument("--label")
|
| 35 |
+
iv = sub.add_parser("image-verify"); iv.add_argument("out")
|
| 36 |
+
ih = sub.add_parser("image-heal"); ih.add_argument("out")
|
| 37 |
+
ir = sub.add_parser("image-restore"); ir.add_argument("out"); ir.add_argument("dst")
|
| 38 |
+
args = ap.parse_args()
|
| 39 |
+
|
| 40 |
+
if args.cmd == "store":
|
| 41 |
+
v = Vault(args.vault, args.k, args.m)
|
| 42 |
+
v.store_folder(args.src)
|
| 43 |
+
s = v.stats()
|
| 44 |
+
print(f"stored {len(v.list_files())} files, {s['unique_chunks']} unique chunks, "
|
| 45 |
+
f"{s['stored']//1024} KB on disk ({s['overhead']:.2f}x, RS {args.k}+{args.m})")
|
| 46 |
+
elif args.cmd == "ls":
|
| 47 |
+
for rel, sz in Vault(args.vault).list_files():
|
| 48 |
+
print(f"{sz:>10} {rel}")
|
| 49 |
+
elif args.cmd == "stat":
|
| 50 |
+
s = Vault(args.vault).stats()
|
| 51 |
+
print(f"logical {s['logical']//1024} KB | stored {s['stored']//1024} KB | "
|
| 52 |
+
f"{s['unique_chunks']} chunks | overhead {s['overhead']:.2f}x")
|
| 53 |
+
elif args.cmd == "heal":
|
| 54 |
+
print(f"regenerated {Vault(args.vault).heal()} missing shard(s)")
|
| 55 |
+
elif args.cmd == "export":
|
| 56 |
+
print(f"exported {Vault(args.vault).export(args.dst)} files")
|
| 57 |
+
elif args.cmd == "mount":
|
| 58 |
+
if not winfsp_available():
|
| 59 |
+
print("WinFsp/winfspy not installed. Install WinFsp (https://winfsp.dev) "
|
| 60 |
+
"and `pip install winfspy`, or use `export` for now.")
|
| 61 |
+
return
|
| 62 |
+
from storage.vaultfs import mount
|
| 63 |
+
fs = mount(Vault(args.vault), args.mountpoint)
|
| 64 |
+
print(f"mounted vault at {args.mountpoint} (Ctrl+C to unmount)")
|
| 65 |
+
try:
|
| 66 |
+
import time
|
| 67 |
+
while True:
|
| 68 |
+
time.sleep(1)
|
| 69 |
+
except KeyboardInterrupt:
|
| 70 |
+
fs.stop()
|
| 71 |
+
print("unmounted")
|
| 72 |
+
elif args.cmd == "image":
|
| 73 |
+
r = diskimage.create(args.device, args.out, k=args.k, m=args.m,
|
| 74 |
+
part_mb=args.part_mb, label=args.label)
|
| 75 |
+
red = (args.k + args.m) / args.k
|
| 76 |
+
print(f"imaged {r['bytes']//1024} KB -> {r['out']} ({r['mode']}, {r['parts']} part(s), "
|
| 77 |
+
f"RS {args.k}+{args.m} ~{red:.2f}x). bad sectors: {r['bad_sectors']}")
|
| 78 |
+
if r["bad_sectors"]:
|
| 79 |
+
print(f" WARNING: {r['bad_sectors']} sectors unreadable at scan (zero-filled).")
|
| 80 |
+
elif args.cmd == "image-verify":
|
| 81 |
+
h = diskimage.verify(args.out)
|
| 82 |
+
print(f"health: {'HEALTHY' if h['healthy'] else 'DAMAGED'} "
|
| 83 |
+
f"chunks={h['chunks']} corrupted_shards={h['corrupted_shards']} "
|
| 84 |
+
f"repairable={h['repairable_chunks']} lost={h['lost_chunks']}")
|
| 85 |
+
elif args.cmd == "image-heal":
|
| 86 |
+
print(f"repaired {diskimage.heal(args.out)} shard(s)")
|
| 87 |
+
elif args.cmd == "image-restore":
|
| 88 |
+
n = diskimage.restore(args.out, args.dst)
|
| 89 |
+
print(f"restored {n//1024} KB -> {args.dst}")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
if __name__ == "__main__":
|
| 93 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
torch
|
step1_storage.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Step 1: verified GF(256) multiply (RS keystone) + content-addressed chunk store.
|
| 3 |
+
|
| 4 |
+
(a) LOG / EXP verified N/N (255/255 each)
|
| 5 |
+
(b) COMPOSED GF(256) multiply verified over all 65,536 (a,b) pairs
|
| 6 |
+
(c) chunk store reconstructs a 'drive image' bit-exact
|
| 7 |
+
(d) dedup removes redundancy (real savings), and is bit-verified by SHA-256
|
| 8 |
+
(e) honesty: incompressible data yields ~no savings (no beating entropy)
|
| 9 |
+
"""
|
| 10 |
+
import os
|
| 11 |
+
import torch
|
| 12 |
+
from storage import gf256
|
| 13 |
+
from storage.common import verify
|
| 14 |
+
from storage.chunkstore import ChunkStore
|
| 15 |
+
|
| 16 |
+
torch.manual_seed(0)
|
| 17 |
+
|
| 18 |
+
print("=" * 62)
|
| 19 |
+
print("STEP 1 -- verified GF(256) + content-addressed chunk store")
|
| 20 |
+
print("=" * 62)
|
| 21 |
+
|
| 22 |
+
# ---- verified coding keystone ----
|
| 23 |
+
net_log, net_exp = gf256.train_units()
|
| 24 |
+
lok, ltot = verify(net_log, *gf256.log_domain())
|
| 25 |
+
eok, etot = verify(net_exp, *gf256.exp_domain())
|
| 26 |
+
mok, mtot = gf256.verify_mul(net_log, net_exp)
|
| 27 |
+
print("-" * 62)
|
| 28 |
+
print(f"(a) LOG verified {lok}/{ltot} | EXP verified {eok}/{etot} -> "
|
| 29 |
+
f"{'PASS' if lok==ltot and eok==etot else 'FAIL'}")
|
| 30 |
+
print(f"(b) COMPOSED GF(256) multiply verified {mok}/{mtot} -> "
|
| 31 |
+
f"{'PASS' if mok==mtot else 'FAIL'}")
|
| 32 |
+
|
| 33 |
+
# ---- content-addressed chunk store on a synthetic 'drive image' ----
|
| 34 |
+
fileA = os.urandom(64 * 1024)
|
| 35 |
+
fileB = os.urandom(64 * 1024)
|
| 36 |
+
fileC = os.urandom(64 * 1024)
|
| 37 |
+
tail = os.urandom(64 * 1024)
|
| 38 |
+
# a drive with duplicated files (A and B appear twice) + unique C + unique tail
|
| 39 |
+
image = fileA + fileB + fileA + fileC + fileB + tail
|
| 40 |
+
|
| 41 |
+
cs = ChunkStore()
|
| 42 |
+
manifest = cs.put_stream(image)
|
| 43 |
+
rebuilt = cs.reconstruct(manifest)
|
| 44 |
+
|
| 45 |
+
exact = rebuilt == image
|
| 46 |
+
print("-" * 62)
|
| 47 |
+
print(f"(c) reconstruct drive image: {'bit-exact PASS' if exact else 'FAIL'} "
|
| 48 |
+
f"({len(image)} bytes)")
|
| 49 |
+
print(f"(d) integrity (all chunks hash-verified): "
|
| 50 |
+
f"{'PASS' if cs.integrity_ok() else 'FAIL'}")
|
| 51 |
+
st = cs.stats()
|
| 52 |
+
print(f" logical {st['logical_bytes']//1024} KB -> stored {st['stored_bytes']//1024} KB "
|
| 53 |
+
f"dedup {st['dedup_ratio']:.2f}x "
|
| 54 |
+
f"({st['unique_chunks']}/{st['total_chunks']} chunks unique)")
|
| 55 |
+
|
| 56 |
+
# ---- honesty check: incompressible random data must NOT shrink ----
|
| 57 |
+
rnd = os.urandom(384 * 1024)
|
| 58 |
+
cs2 = ChunkStore()
|
| 59 |
+
cs2.reconstruct(cs2.put_stream(rnd)) # store it
|
| 60 |
+
st2 = cs2.stats()
|
| 61 |
+
print(f"(e) incompressible 384 KB: dedup {st2['dedup_ratio']:.2f}x "
|
| 62 |
+
f"-> {'PASS (no magic)' if st2['dedup_ratio'] < 1.05 else 'unexpected'}")
|
| 63 |
+
|
| 64 |
+
allpass = (lok == ltot and eok == etot and mok == mtot and exact
|
| 65 |
+
and cs.integrity_ok() and st['dedup_ratio'] > 1.2 and st2['dedup_ratio'] < 1.05)
|
| 66 |
+
print("=" * 62)
|
| 67 |
+
print(f"OVERALL: {'ALL PASS' if allpass else 'some checks failed'}")
|
| 68 |
+
|
| 69 |
+
if lok == ltot and eok == etot and mok == mtot:
|
| 70 |
+
torch.save({"log": net_log.state_dict(), "exp": net_exp.state_dict(),
|
| 71 |
+
"meta": {"unit": "GF256_MUL(log/exp)", "poly": "0x11D",
|
| 72 |
+
"multiply_verified": f"{mok}/{mtot}"}}, "GF256.pt")
|
| 73 |
+
print("saved verified units -> GF256.pt")
|
step2_rs.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Step 2: Reed-Solomon erasure coding on the verified GF(256) core.
|
| 3 |
+
|
| 4 |
+
(a) neural coding equivalence -> parity computed via the NEURAL gf_mul equals
|
| 5 |
+
the golden RS parity (the verified units drive the code)
|
| 6 |
+
(b) any-k-of-n recovery -> RS(k=4, m=2): reconstruct from EVERY possible
|
| 7 |
+
2-shard-loss pattern, bit-exact
|
| 8 |
+
(c) shards are bit-verified -> each shard addressed by SHA-256
|
| 9 |
+
(d) honesty -> n shards total (k+m)/k x the data (redundancy,
|
| 10 |
+
not shrinkage)
|
| 11 |
+
"""
|
| 12 |
+
import os
|
| 13 |
+
import hashlib
|
| 14 |
+
import itertools
|
| 15 |
+
import torch
|
| 16 |
+
from storage import gf256, rs
|
| 17 |
+
from storage.common import verify
|
| 18 |
+
|
| 19 |
+
torch.manual_seed(0)
|
| 20 |
+
|
| 21 |
+
print("=" * 62)
|
| 22 |
+
print("STEP 2 -- Reed-Solomon erasure coding (verified GF core)")
|
| 23 |
+
print("=" * 62)
|
| 24 |
+
|
| 25 |
+
# load or train the verified GF units
|
| 26 |
+
try:
|
| 27 |
+
ck = torch.load("GF256.pt", map_location="cpu")
|
| 28 |
+
from storage.common import mlp
|
| 29 |
+
net_log = mlp(8, 8, 256, 2); net_log.load_state_dict(ck["log"])
|
| 30 |
+
net_exp = mlp(8, 8, 256, 2); net_exp.load_state_dict(ck["exp"])
|
| 31 |
+
print("loaded verified GF units from GF256.pt")
|
| 32 |
+
except Exception:
|
| 33 |
+
net_log, net_exp = gf256.train_units()
|
| 34 |
+
|
| 35 |
+
# (a) neural coding equivalence
|
| 36 |
+
neq = rs.neural_parity_equiv(net_log, net_exp)
|
| 37 |
+
print(f"(a) neural gf_mul parity == golden RS parity -> {'PASS' if neq else 'FAIL'}")
|
| 38 |
+
|
| 39 |
+
# (b) any-k-of-n recovery over a real 'drive image'
|
| 40 |
+
K, M = 4, 2
|
| 41 |
+
N = K + M
|
| 42 |
+
image = os.urandom(300 * 1024) # 300 KB payload
|
| 43 |
+
shards, L, orig = rs.encode(image, K, M)
|
| 44 |
+
print(f" RS({K}+{M}): {N} shards of {L//1024} KB each (tolerates up to {M} lost)")
|
| 45 |
+
|
| 46 |
+
fails = 0
|
| 47 |
+
patterns = list(itertools.combinations(range(N), M)) # every 2-shard-loss case
|
| 48 |
+
for lost in patterns:
|
| 49 |
+
present = {i: bytes(shards[i]) for i in range(N) if i not in lost}
|
| 50 |
+
if rs.decode(present, K, M, L, orig) != image:
|
| 51 |
+
fails += 1
|
| 52 |
+
print(f"(b) recover from every {M}-loss pattern ({len(patterns)} of them): "
|
| 53 |
+
f"{'PASS' if fails == 0 else f'FAIL({fails})'}")
|
| 54 |
+
|
| 55 |
+
# (c) shards bit-verified by SHA-256
|
| 56 |
+
ids = [hashlib.sha256(bytes(s)).hexdigest()[:12] for s in shards]
|
| 57 |
+
verified = all(hashlib.sha256(bytes(shards[i])).hexdigest()[:12] == ids[i] for i in range(N))
|
| 58 |
+
print(f"(c) shard hashes (bit-verified): {'PASS' if verified else 'FAIL'} "
|
| 59 |
+
f"e.g. shard0={ids[0]}")
|
| 60 |
+
|
| 61 |
+
# (d) honesty: storage overhead is exactly the redundancy, nothing shrinks
|
| 62 |
+
overhead = N * L / len(image)
|
| 63 |
+
print(f"(d) storage: {len(image)//1024} KB -> {N*L//1024} KB ({overhead:.2f}x) "
|
| 64 |
+
f"-> {'PASS (redundancy, no shrink)' if overhead >= 1.0 else 'FAIL'}")
|
| 65 |
+
|
| 66 |
+
allpass = neq and fails == 0 and verified and overhead >= 1.0
|
| 67 |
+
print("=" * 62)
|
| 68 |
+
print(f"OVERALL: {'ALL PASS' if allpass else 'some checks failed'}")
|
step3_vault.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Step 3: the Vault -- dedup + RS-protected store that reconstructs files on demand
|
| 3 |
+
and survives shard loss. Runs with no external dependencies (WinFsp only needed
|
| 4 |
+
for the live drive-letter mount).
|
| 5 |
+
|
| 6 |
+
(a) store a folder (with duplicate files) -> dedup
|
| 7 |
+
(b) reconstruct every file bit-exact via the read layer (VaultFS)
|
| 8 |
+
(c) DAMAGE: delete up to m shards from chunks -> files still read (RS-heal)
|
| 9 |
+
(d) heal() regenerates the missing shards; integrity restored
|
| 10 |
+
"""
|
| 11 |
+
import os
|
| 12 |
+
import random
|
| 13 |
+
import shutil
|
| 14 |
+
import tempfile
|
| 15 |
+
from storage.vault import Vault
|
| 16 |
+
from storage.vaultfs import VaultFS, winfsp_available
|
| 17 |
+
|
| 18 |
+
random.seed(0)
|
| 19 |
+
root = tempfile.mkdtemp(prefix="nvault_")
|
| 20 |
+
src = os.path.join(root, "src"); os.makedirs(os.path.join(src, "sub"))
|
| 21 |
+
vault_dir = os.path.join(root, "vault")
|
| 22 |
+
dst = os.path.join(root, "restored")
|
| 23 |
+
|
| 24 |
+
print("=" * 62)
|
| 25 |
+
print("STEP 3 -- Vault: dedup + RS-protected virtual storage")
|
| 26 |
+
print("=" * 62)
|
| 27 |
+
|
| 28 |
+
# build a source folder with a duplicated file (photo.bin appears twice)
|
| 29 |
+
photo = os.urandom(120 * 1024)
|
| 30 |
+
files = {
|
| 31 |
+
"photo.bin": photo,
|
| 32 |
+
"sub/photo_copy.bin": photo, # exact duplicate -> dedup
|
| 33 |
+
"notes.txt": ("neural vault demo\n" * 4000).encode(),
|
| 34 |
+
"random.bin": os.urandom(80 * 1024),
|
| 35 |
+
}
|
| 36 |
+
for rel, data in files.items():
|
| 37 |
+
p = os.path.join(src, rel)
|
| 38 |
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
| 39 |
+
open(p, "wb").write(data)
|
| 40 |
+
|
| 41 |
+
# (a) store
|
| 42 |
+
v = Vault(vault_dir, k=4, m=2)
|
| 43 |
+
v.store_folder(src)
|
| 44 |
+
s = v.stats()
|
| 45 |
+
logical_files = sum(len(d) for d in files.values())
|
| 46 |
+
print(f"(a) stored {len(v.list_files())} files, {s['unique_chunks']} unique chunks; "
|
| 47 |
+
f"logical {logical_files//1024} KB, on-disk {s['stored']//1024} KB "
|
| 48 |
+
f"(RS 4+2 => ~1.5x of DEDUPED data)")
|
| 49 |
+
|
| 50 |
+
# (b) reconstruct via read layer, compare to originals
|
| 51 |
+
fs = VaultFS(v)
|
| 52 |
+
ok = all(fs.read(rel) == files[rel] for rel in files)
|
| 53 |
+
print(f"(b) reconstruct every file bit-exact (VaultFS.read): {'PASS' if ok else 'FAIL'}")
|
| 54 |
+
|
| 55 |
+
# (c) DAMAGE: delete up to m=2 shards from each chunk, then read anyway
|
| 56 |
+
deleted = 0
|
| 57 |
+
for h in os.listdir(v.cdir):
|
| 58 |
+
d = v._cd(h)
|
| 59 |
+
if not os.path.isfile(os.path.join(d, "meta.json")):
|
| 60 |
+
continue
|
| 61 |
+
shard_files = [f for f in os.listdir(d) if f.startswith("shard_")]
|
| 62 |
+
for f in random.sample(shard_files, k=random.randint(1, 2)): # <= m
|
| 63 |
+
os.remove(os.path.join(d, f)); deleted += 1
|
| 64 |
+
fs2 = VaultFS(v) # fresh (no cache)
|
| 65 |
+
survived = all(fs2.read(rel) == files[rel] for rel in files)
|
| 66 |
+
print(f"(c) deleted {deleted} shard files (<=2/chunk); files still read: "
|
| 67 |
+
f"{'PASS (RS-healed on read)' if survived else 'FAIL'}")
|
| 68 |
+
|
| 69 |
+
# (d) heal regenerates missing shards
|
| 70 |
+
healed = v.heal()
|
| 71 |
+
full = v.integrity_ok()
|
| 72 |
+
print(f"(d) heal() regenerated {healed} shard(s); integrity restored: "
|
| 73 |
+
f"{'PASS' if full else 'FAIL'}")
|
| 74 |
+
|
| 75 |
+
# export as final proof
|
| 76 |
+
n = v.export(dst)
|
| 77 |
+
export_ok = all(open(os.path.join(dst, rel), "rb").read() == files[rel] for rel in files)
|
| 78 |
+
print(f" export {n} files -> {'bit-exact PASS' if export_ok else 'FAIL'}")
|
| 79 |
+
|
| 80 |
+
print("-" * 62)
|
| 81 |
+
print(f"WinFsp drive-letter mount available on this machine: {winfsp_available()} "
|
| 82 |
+
f"({'ready: python cli.py mount <vault> X:' if winfsp_available() else 'use: python cli.py export <vault> <folder>'})")
|
| 83 |
+
|
| 84 |
+
allpass = ok and survived and full and export_ok
|
| 85 |
+
print("=" * 62)
|
| 86 |
+
print(f"OVERALL: {'ALL PASS' if allpass else 'some checks failed'}")
|
| 87 |
+
shutil.rmtree(root, ignore_errors=True)
|
storage/__init__.py
ADDED
|
File without changes
|
storage/archive.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Self-healing .pt disc archive.
|
| 3 |
+
|
| 4 |
+
A disc image is chunked (fixed size), duplicate chunks deduped, and each unique
|
| 5 |
+
chunk Reed-Solomon split into n = k+m shards (verified GF(256) core). Every shard
|
| 6 |
+
carries its own SHA-256, so SILENT corruption (bit-rot flipping bytes) is
|
| 7 |
+
DETECTED, marked as an erasure, and REPAIRED from the surviving shards -- as long
|
| 8 |
+
as at least k of the n shards per chunk are still intact.
|
| 9 |
+
|
| 10 |
+
The whole thing serializes to a single .pt (torch.save):
|
| 11 |
+
{ meta, manifest:[chunk_hash...], chunks:{ hash: {shards, shard_hashes, L, orig} } }
|
| 12 |
+
|
| 13 |
+
Honesty: RS adds redundancy (archive is ~(k+m)/k x the deduped image). It repairs
|
| 14 |
+
up to m damaged shards per chunk; beyond that a chunk is flagged as lost, never
|
| 15 |
+
silently wrong. No entropy is beaten.
|
| 16 |
+
"""
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
import hashlib
|
| 19 |
+
import time
|
| 20 |
+
import torch
|
| 21 |
+
from . import rs
|
| 22 |
+
|
| 23 |
+
CHUNK = 4096
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _h(b: bytes) -> str:
|
| 27 |
+
return hashlib.sha256(b).hexdigest()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def build(data: bytes, label="disc", k=4, m=2, chunk=CHUNK, bad_sectors=None) -> dict:
|
| 31 |
+
chunks = {}
|
| 32 |
+
manifest = []
|
| 33 |
+
for off in range(0, len(data), chunk):
|
| 34 |
+
ch = data[off:off + chunk]
|
| 35 |
+
h = _h(ch)
|
| 36 |
+
manifest.append(h)
|
| 37 |
+
if h in chunks:
|
| 38 |
+
continue
|
| 39 |
+
shards, L, orig = rs.encode(ch, k, m)
|
| 40 |
+
shards = [bytes(s) for s in shards]
|
| 41 |
+
chunks[h] = {"shards": shards, "shard_hashes": [_h(s) for s in shards],
|
| 42 |
+
"L": L, "orig": orig, "k": k, "m": m}
|
| 43 |
+
return {
|
| 44 |
+
"meta": {"label": label, "size": len(data), "chunk": chunk, "k": k, "m": m,
|
| 45 |
+
"bad_sectors": list(bad_sectors or []), "created": time.time(),
|
| 46 |
+
"format": "neural-cd-preserve/1"},
|
| 47 |
+
"manifest": manifest, "chunks": chunks,
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def save(arc: dict, path: str):
|
| 52 |
+
torch.save(arc, path)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def load(path: str) -> dict:
|
| 56 |
+
return torch.load(path, map_location="cpu", weights_only=False)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _good_shards(rec: dict) -> dict:
|
| 60 |
+
"""{index: bytes} for shards whose hash still matches (uncorrupted)."""
|
| 61 |
+
out = {}
|
| 62 |
+
for i, s in enumerate(rec["shards"]):
|
| 63 |
+
if _h(s) == rec["shard_hashes"][i]:
|
| 64 |
+
out[i] = s
|
| 65 |
+
return out
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def verify(arc: dict) -> dict:
|
| 69 |
+
"""Report health without modifying. -> {chunks, corrupted_shards, repairable,
|
| 70 |
+
lost} where lost chunks have < k intact shards (unrecoverable)."""
|
| 71 |
+
corrupted = repairable = lost = 0
|
| 72 |
+
for h, rec in arc["chunks"].items():
|
| 73 |
+
good = _good_shards(rec)
|
| 74 |
+
nbad = len(rec["shards"]) - len(good)
|
| 75 |
+
corrupted += nbad
|
| 76 |
+
if nbad and len(good) >= rec["k"]:
|
| 77 |
+
repairable += 1
|
| 78 |
+
if len(good) < rec["k"]:
|
| 79 |
+
lost += 1
|
| 80 |
+
return {"chunks": len(arc["chunks"]), "corrupted_shards": corrupted,
|
| 81 |
+
"repairable_chunks": repairable, "lost_chunks": lost,
|
| 82 |
+
"healthy": corrupted == 0 and lost == 0}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def heal(arc: dict) -> int:
|
| 86 |
+
"""Detect corrupted shards and regenerate them from survivors. Returns the
|
| 87 |
+
number of shards repaired. Chunks with < k intact shards cannot be healed."""
|
| 88 |
+
repaired = 0
|
| 89 |
+
for h, rec in arc["chunks"].items():
|
| 90 |
+
good = _good_shards(rec)
|
| 91 |
+
if len(good) == len(rec["shards"]):
|
| 92 |
+
continue
|
| 93 |
+
if len(good) < rec["k"]:
|
| 94 |
+
continue # unrecoverable, leave as-is
|
| 95 |
+
ch = rs.decode(good, rec["k"], rec["m"], rec["L"], rec["orig"])
|
| 96 |
+
if _h(ch) != h:
|
| 97 |
+
continue # decoded wrong -> don't trust
|
| 98 |
+
fresh, _, _ = rs.encode(ch, rec["k"], rec["m"])
|
| 99 |
+
for i in range(len(rec["shards"])):
|
| 100 |
+
if i not in good:
|
| 101 |
+
rec["shards"][i] = bytes(fresh[i])
|
| 102 |
+
rec["shard_hashes"][i] = _h(rec["shards"][i])
|
| 103 |
+
repaired += 1
|
| 104 |
+
return repaired
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def restore(arc: dict) -> bytes:
|
| 108 |
+
"""Reconstruct the disc image, RS-correcting any detected corruption."""
|
| 109 |
+
out = bytearray()
|
| 110 |
+
for h in arc["manifest"]:
|
| 111 |
+
rec = arc["chunks"][h]
|
| 112 |
+
good = _good_shards(rec)
|
| 113 |
+
if len(good) < rec["k"]:
|
| 114 |
+
raise IOError(f"chunk {h[:8]}: only {len(good)} intact shards, "
|
| 115 |
+
f"need {rec['k']} -- unrecoverable")
|
| 116 |
+
ch = rs.decode(good, rec["k"], rec["m"], rec["L"], rec["orig"])
|
| 117 |
+
if _h(ch) != h:
|
| 118 |
+
raise IOError(f"chunk {h[:8]}: hash mismatch after decode")
|
| 119 |
+
out += ch
|
| 120 |
+
return bytes(out[:arc["meta"]["size"]])
|
storage/chunkstore.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Content-addressed, deduplicating chunk store.
|
| 3 |
+
|
| 4 |
+
* content-defined chunking (Gear-hash CDC) splits a byte stream into
|
| 5 |
+
variable-length chunks at data-dependent boundaries -- so duplicate regions
|
| 6 |
+
chunk identically even if shifted.
|
| 7 |
+
* each chunk's SHA-256 is its address and its bit-verification.
|
| 8 |
+
* identical chunks are stored once (dedup). A manifest (list of hashes)
|
| 9 |
+
reconstructs the original stream exactly.
|
| 10 |
+
|
| 11 |
+
Honest note: dedup + (optional) compression only removes REDUNDANT/compressible
|
| 12 |
+
data. Incompressible or unique data is stored in full -- you cannot beat entropy.
|
| 13 |
+
The chunk hashes are real crypto (SHA-256), not a neural emulation.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
import hashlib
|
| 17 |
+
import random
|
| 18 |
+
|
| 19 |
+
# Gear table: 256 fixed 64-bit values (deterministic).
|
| 20 |
+
_rng = random.Random(0xC0FFEE)
|
| 21 |
+
GEAR = [_rng.getrandbits(64) for _ in range(256)]
|
| 22 |
+
MASK64 = (1 << 64) - 1
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def chunk_stream(data: bytes, min_sz=2048, avg_bits=13, max_sz=65536):
|
| 26 |
+
"""Yield (offset, length) chunk boundaries via Gear-hash CDC.
|
| 27 |
+
avg_bits sets average chunk size ~ 2**avg_bits bytes."""
|
| 28 |
+
mask = (1 << avg_bits) - 1
|
| 29 |
+
n = len(data)
|
| 30 |
+
start = 0
|
| 31 |
+
while start < n:
|
| 32 |
+
h = 0
|
| 33 |
+
i = start
|
| 34 |
+
end = min(start + max_sz, n)
|
| 35 |
+
while i < end:
|
| 36 |
+
h = ((h << 1) + GEAR[data[i]]) & MASK64
|
| 37 |
+
i += 1
|
| 38 |
+
if i - start >= min_sz and (h & mask) == 0:
|
| 39 |
+
break
|
| 40 |
+
yield start, i - start
|
| 41 |
+
start = i
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class ChunkStore:
|
| 45 |
+
def __init__(self):
|
| 46 |
+
self.store: dict[str, bytes] = {} # sha256 -> chunk bytes
|
| 47 |
+
self.logical_bytes = 0
|
| 48 |
+
self.total_chunks = 0
|
| 49 |
+
|
| 50 |
+
def put_stream(self, data: bytes) -> list[str]:
|
| 51 |
+
manifest = []
|
| 52 |
+
self.logical_bytes += len(data)
|
| 53 |
+
for off, ln in chunk_stream(data):
|
| 54 |
+
ch = data[off:off + ln]
|
| 55 |
+
h = hashlib.sha256(ch).hexdigest()
|
| 56 |
+
if h not in self.store:
|
| 57 |
+
self.store[h] = ch
|
| 58 |
+
manifest.append(h)
|
| 59 |
+
self.total_chunks += 1
|
| 60 |
+
return manifest
|
| 61 |
+
|
| 62 |
+
def reconstruct(self, manifest: list[str]) -> bytes:
|
| 63 |
+
return b"".join(self.store[h] for h in manifest)
|
| 64 |
+
|
| 65 |
+
def integrity_ok(self) -> bool:
|
| 66 |
+
"""Every stored chunk must still hash to its address (bit-verified)."""
|
| 67 |
+
return all(hashlib.sha256(v).hexdigest() == k for k, v in self.store.items())
|
| 68 |
+
|
| 69 |
+
def stored_bytes(self) -> int:
|
| 70 |
+
return sum(len(v) for v in self.store.values())
|
| 71 |
+
|
| 72 |
+
def stats(self) -> dict:
|
| 73 |
+
return {
|
| 74 |
+
"logical_bytes": self.logical_bytes,
|
| 75 |
+
"stored_bytes": self.stored_bytes(),
|
| 76 |
+
"total_chunks": self.total_chunks,
|
| 77 |
+
"unique_chunks": len(self.store),
|
| 78 |
+
"dedup_ratio": self.logical_bytes / max(1, self.stored_bytes()),
|
| 79 |
+
}
|
storage/common.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared helpers (same as the neural-DDR project): bit<->int, MLP, verify, train."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
|
| 6 |
+
DEV = "cuda" if torch.cuda.is_available() else "cpu"
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def bits_of(v: int, n: int) -> list[int]:
|
| 10 |
+
return [(v >> k) & 1 for k in range(n)]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def int_of(bits) -> int:
|
| 14 |
+
return sum((1 << k) for k, b in enumerate(bits) if b > 0)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def pm(bits) -> torch.Tensor:
|
| 18 |
+
return torch.tensor([1.0 if b else -1.0 for b in bits], dtype=torch.float32)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def mlp(inp: int, out: int, h: int = 256, layers: int = 2) -> nn.Sequential:
|
| 22 |
+
mods = [nn.Linear(inp, h), nn.GELU()]
|
| 23 |
+
for _ in range(layers - 1):
|
| 24 |
+
mods += [nn.Linear(h, h), nn.GELU()]
|
| 25 |
+
mods += [nn.Linear(h, out)]
|
| 26 |
+
return nn.Sequential(*mods)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@torch.no_grad()
|
| 30 |
+
def verify(net, X, Ybits) -> tuple[int, int]:
|
| 31 |
+
net = net.to("cpu")
|
| 32 |
+
pred = (net(X) > 0).int()
|
| 33 |
+
ok = (pred == Ybits.int()).all(dim=1).sum().item()
|
| 34 |
+
return ok, X.shape[0]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def train(net, X, Ybits, steps=8000, lr=2e-3, tag="", report=2000):
|
| 38 |
+
net = net.to(DEV)
|
| 39 |
+
Xd, Yd = X.to(DEV), Ybits.to(DEV)
|
| 40 |
+
opt = torch.optim.Adam(net.parameters(), lr=lr)
|
| 41 |
+
sch = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=steps)
|
| 42 |
+
lossfn = nn.BCEWithLogitsLoss()
|
| 43 |
+
for e in range(steps):
|
| 44 |
+
opt.zero_grad()
|
| 45 |
+
loss = lossfn(net(Xd), Yd)
|
| 46 |
+
loss.backward(); opt.step(); sch.step()
|
| 47 |
+
if e % report == 0 or e == steps - 1:
|
| 48 |
+
ok, tot = verify(net, X, Ybits); net.to(DEV)
|
| 49 |
+
print(f" [{tag}] epoch {e:5d} loss {loss.item():.2e} verified {ok}/{tot}")
|
| 50 |
+
if ok == tot:
|
| 51 |
+
print(f" [{tag}] -> N/N"); break
|
| 52 |
+
return net.to("cpu")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def run8(net, v: int) -> int:
|
| 56 |
+
return int_of((net(pm(bits_of(v, 8)).unsqueeze(0))[0] > 0).int().tolist())
|
storage/diskimage.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Whole-drive imaging into a self-healing .pt.
|
| 3 |
+
|
| 4 |
+
Scans a block device (Windows: r"\\\\.\\C:" or r"\\\\.\\PhysicalDrive0"; may need
|
| 5 |
+
Administrator) or an image file, and turns it into a self-healing .pt archive:
|
| 6 |
+
content-defined chunks, Reed-Solomon shards, per-shard SHA-256 (Step 1-2 +
|
| 7 |
+
archive.py). It self-heals bit-rot in the archive and reconstructs the image
|
| 8 |
+
bit-exact.
|
| 9 |
+
|
| 10 |
+
Streaming by segment keeps memory bounded, so it scales past RAM:
|
| 11 |
+
* small input (<= one segment) -> a single self-contained <out>.pt
|
| 12 |
+
* large drive -> <out>.pt manifest + <out>.pt.part0, part1, ...
|
| 13 |
+
|
| 14 |
+
HONEST LIMITS (read these):
|
| 15 |
+
* RS adds redundancy: the .pt is ~ (k+m)/k x the imaged data (e.g. 1.5x). This
|
| 16 |
+
does NOT shrink a drive -- you cannot beat entropy.
|
| 17 |
+
* Imaging an "entire HDD" means writing >= 1.5x its used size to disk and takes
|
| 18 |
+
real time; use --part-mb and expect a big output.
|
| 19 |
+
* Raw whole-disk reads need Administrator and the disk should be idle/unmounted
|
| 20 |
+
(image a partition, or a disk you are not writing to).
|
| 21 |
+
* RS heals the ARCHIVE against future bit-rot. Sectors unreadable at scan time
|
| 22 |
+
are recorded as bad, not invented.
|
| 23 |
+
"""
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
import os
|
| 26 |
+
import torch
|
| 27 |
+
from . import archive, scan
|
| 28 |
+
|
| 29 |
+
DEFAULT_PART = 64 * 1024 * 1024 # 64 MB segments
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _read_segment(f, part_bytes: int, sector: int):
|
| 33 |
+
"""Read up to part_bytes in sector-sized reads. -> (data, bad_sectors, eof)."""
|
| 34 |
+
data = bytearray()
|
| 35 |
+
bad = []
|
| 36 |
+
idx = 0
|
| 37 |
+
while len(data) < part_bytes:
|
| 38 |
+
try:
|
| 39 |
+
blk = f.read(sector)
|
| 40 |
+
except OSError:
|
| 41 |
+
bad.append(idx)
|
| 42 |
+
data += bytes(sector)
|
| 43 |
+
idx += 1
|
| 44 |
+
try:
|
| 45 |
+
f.seek(sector, os.SEEK_CUR)
|
| 46 |
+
except OSError:
|
| 47 |
+
return bytes(data), bad, True
|
| 48 |
+
continue
|
| 49 |
+
if not blk:
|
| 50 |
+
return bytes(data), bad, True
|
| 51 |
+
data += blk
|
| 52 |
+
idx += 1
|
| 53 |
+
if len(blk) < sector:
|
| 54 |
+
return bytes(data), bad, True
|
| 55 |
+
return bytes(data), bad, False
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def create(device: str, out_pt: str, k: int = 4, m: int = 2,
|
| 59 |
+
chunk: int = archive.CHUNK, part_mb: int = 64,
|
| 60 |
+
sector: int = scan.SECTOR, label: str | None = None) -> dict:
|
| 61 |
+
label = label or os.path.basename(device.rstrip("\\/")) or "disk"
|
| 62 |
+
part_bytes = part_mb * 1024 * 1024
|
| 63 |
+
with open(device, "rb", buffering=0) as f:
|
| 64 |
+
seg, bad, eof = _read_segment(f, part_bytes, sector)
|
| 65 |
+
if eof: # whole device fit in one segment
|
| 66 |
+
arc = archive.build(seg, label=label, k=k, m=m, bad_sectors=bad)
|
| 67 |
+
arc["meta"]["device"] = device
|
| 68 |
+
torch.save(arc, out_pt)
|
| 69 |
+
return {"mode": "single", "bytes": len(seg), "bad_sectors": len(bad),
|
| 70 |
+
"parts": 1, "out": out_pt}
|
| 71 |
+
parts, n, total_bad = [], 0, 0
|
| 72 |
+
while True:
|
| 73 |
+
p = f"{out_pt}.part{n}"
|
| 74 |
+
arc = archive.build(seg, label=f"{label}.part{n}", k=k, m=m, bad_sectors=bad)
|
| 75 |
+
torch.save(arc, p)
|
| 76 |
+
parts.append({"path": os.path.basename(p), "size": len(seg)})
|
| 77 |
+
total_bad += len(bad)
|
| 78 |
+
n += 1
|
| 79 |
+
if eof:
|
| 80 |
+
break
|
| 81 |
+
seg, bad, eof = _read_segment(f, part_bytes, sector)
|
| 82 |
+
if not seg and eof:
|
| 83 |
+
break
|
| 84 |
+
manifest = {"format": "neural-storage/diskimage-1", "device": device, "label": label,
|
| 85 |
+
"k": k, "m": m, "chunk": chunk, "parts": parts,
|
| 86 |
+
"total": sum(p["size"] for p in parts), "bad_sectors": total_bad}
|
| 87 |
+
torch.save(manifest, out_pt)
|
| 88 |
+
return {"mode": "multi", "bytes": manifest["total"], "bad_sectors": total_bad,
|
| 89 |
+
"parts": len(parts), "out": out_pt}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _is_manifest(obj) -> bool:
|
| 93 |
+
return isinstance(obj, dict) and obj.get("format", "").startswith("neural-storage/diskimage")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def verify(out_pt: str) -> dict:
|
| 97 |
+
obj = archive.load(out_pt)
|
| 98 |
+
if not _is_manifest(obj):
|
| 99 |
+
return archive.verify(obj)
|
| 100 |
+
base = os.path.dirname(out_pt)
|
| 101 |
+
agg = {"chunks": 0, "corrupted_shards": 0, "repairable_chunks": 0, "lost_chunks": 0}
|
| 102 |
+
for p in obj["parts"]:
|
| 103 |
+
h = archive.verify(archive.load(os.path.join(base, p["path"])))
|
| 104 |
+
for kk in agg:
|
| 105 |
+
agg[kk] += h[kk]
|
| 106 |
+
agg["healthy"] = agg["corrupted_shards"] == 0 and agg["lost_chunks"] == 0
|
| 107 |
+
return agg
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def heal(out_pt: str) -> int:
|
| 111 |
+
obj = archive.load(out_pt)
|
| 112 |
+
if not _is_manifest(obj):
|
| 113 |
+
n = archive.heal(obj)
|
| 114 |
+
torch.save(obj, out_pt)
|
| 115 |
+
return n
|
| 116 |
+
base = os.path.dirname(out_pt)
|
| 117 |
+
total = 0
|
| 118 |
+
for p in obj["parts"]:
|
| 119 |
+
pth = os.path.join(base, p["path"])
|
| 120 |
+
arc = archive.load(pth)
|
| 121 |
+
n = archive.heal(arc)
|
| 122 |
+
if n:
|
| 123 |
+
torch.save(arc, pth)
|
| 124 |
+
total += n
|
| 125 |
+
return total
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def restore(out_pt: str, out_file: str) -> int:
|
| 129 |
+
obj = archive.load(out_pt)
|
| 130 |
+
with open(out_file, "wb") as w:
|
| 131 |
+
if not _is_manifest(obj):
|
| 132 |
+
data = archive.restore(obj)
|
| 133 |
+
w.write(data)
|
| 134 |
+
return len(data)
|
| 135 |
+
base = os.path.dirname(out_pt)
|
| 136 |
+
total = 0
|
| 137 |
+
for p in obj["parts"]:
|
| 138 |
+
data = archive.restore(archive.load(os.path.join(base, p["path"])))
|
| 139 |
+
w.write(data)
|
| 140 |
+
total += len(data)
|
| 141 |
+
return total
|
storage/gf256.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Step 1 keystone: verified GF(2^8) multiply -- the arithmetic core of Reed-Solomon
|
| 3 |
+
erasure coding (Step 2).
|
| 4 |
+
|
| 5 |
+
A direct 16-bit -> 8-bit neural multiply is hard to drive to N/N. So we decompose
|
| 6 |
+
it the way real RS implementations do -- via log/exp tables:
|
| 7 |
+
|
| 8 |
+
a * b = EXP[ (LOG[a] + LOG[b]) mod 255 ] (and 0 if a==0 or b==0)
|
| 9 |
+
|
| 10 |
+
LOG and EXP are 256-entry lookups -> trivially N/N verifiable neural units. We
|
| 11 |
+
train both, compose them into a full multiply, and verify the COMPOSED multiply
|
| 12 |
+
against the golden field over ALL 65,536 (a,b) pairs.
|
| 13 |
+
|
| 14 |
+
Field: GF(2^8), reduction polynomial 0x11D, generator 2 (standard for RS).
|
| 15 |
+
"""
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
import torch
|
| 18 |
+
from .common import mlp, train, verify, pm, bits_of, int_of, run8
|
| 19 |
+
|
| 20 |
+
POLY = 0x11D
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def build_tables():
|
| 24 |
+
exp = [0] * 512
|
| 25 |
+
log = [0] * 256
|
| 26 |
+
x = 1
|
| 27 |
+
for i in range(255):
|
| 28 |
+
exp[i] = x
|
| 29 |
+
log[x] = i
|
| 30 |
+
x <<= 1
|
| 31 |
+
if x & 0x100:
|
| 32 |
+
x ^= POLY
|
| 33 |
+
for i in range(255, 512):
|
| 34 |
+
exp[i] = exp[i - 255]
|
| 35 |
+
return exp, log
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
EXP, LOG = build_tables()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def gf_mul(a: int, b: int) -> int:
|
| 42 |
+
if a == 0 or b == 0:
|
| 43 |
+
return 0
|
| 44 |
+
return EXP[LOG[a] + LOG[b]]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ---- neural LOG (inputs 1..255) and EXP (indices 0..254) ----
|
| 48 |
+
def log_domain():
|
| 49 |
+
xs = [pm(bits_of(a, 8)) for a in range(1, 256)]
|
| 50 |
+
ys = [[float(b) for b in bits_of(LOG[a], 8)] for a in range(1, 256)]
|
| 51 |
+
return torch.stack(xs), torch.tensor(ys)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def exp_domain():
|
| 55 |
+
xs = [pm(bits_of(i, 8)) for i in range(255)]
|
| 56 |
+
ys = [[float(b) for b in bits_of(EXP[i], 8)] for i in range(255)]
|
| 57 |
+
return torch.stack(xs), torch.tensor(ys)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def gf_mul_neural(a: int, b: int, net_log, net_exp) -> int:
|
| 61 |
+
if a == 0 or b == 0:
|
| 62 |
+
return 0
|
| 63 |
+
s = (run8(net_log, a) + run8(net_log, b)) % 255
|
| 64 |
+
return run8(net_exp, s)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def train_units():
|
| 68 |
+
print(" training LOG (8->8):")
|
| 69 |
+
net_log = train(mlp(8, 8, 256, 2), *log_domain(), steps=8000, tag="log")
|
| 70 |
+
print(" training EXP (8->8):")
|
| 71 |
+
net_exp = train(mlp(8, 8, 256, 2), *exp_domain(), steps=8000, tag="exp")
|
| 72 |
+
return net_log, net_exp
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def verify_mul(net_log, net_exp) -> tuple[int, int]:
|
| 76 |
+
ok = 0
|
| 77 |
+
for a in range(256):
|
| 78 |
+
for b in range(256):
|
| 79 |
+
ok += (gf_mul_neural(a, b, net_log, net_exp) == gf_mul(a, b))
|
| 80 |
+
return ok, 256 * 256
|
storage/rs.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Step 2: Reed-Solomon erasure coding over the verified GF(256) field.
|
| 3 |
+
|
| 4 |
+
Split data into k systematic shards + m parity shards (n = k+m). ANY k of the n
|
| 5 |
+
shards reconstruct the original -- so up to m shards can be lost or corrupted and
|
| 6 |
+
the data still survives. This is the honest sense of "small pieces that together
|
| 7 |
+
rebuild the whole": redundancy, not shrinkage (n shards total n/k x the data).
|
| 8 |
+
|
| 9 |
+
All coefficients come from a Cauchy matrix over GF(256) (every square submatrix
|
| 10 |
+
invertible => any-k-of-n recovery). Every multiply is the GF(256) multiply that
|
| 11 |
+
the neural LOG/EXP units were verified bit-exact against (65536/65536), so the
|
| 12 |
+
fast table path here is the proven-equivalent of the neural coding core.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
from .gf256 import EXP, LOG, gf_mul
|
| 16 |
+
|
| 17 |
+
MUL = [[gf_mul(a, b) for b in range(256)] for a in range(256)] # full product table
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def gf_inv(a: int) -> int:
|
| 21 |
+
return EXP[255 - LOG[a]] if a else 0
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def cauchy(k: int, m: int):
|
| 25 |
+
return [[gf_inv((k + i) ^ j) for j in range(k)] for i in range(m)]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _gen_row(r: int, k: int, C):
|
| 29 |
+
if r < k:
|
| 30 |
+
row = [0] * k
|
| 31 |
+
row[r] = 1
|
| 32 |
+
return row
|
| 33 |
+
return C[r - k]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def encode(data: bytes, k: int, m: int):
|
| 37 |
+
"""-> (shards[n], shard_len, orig_len). shards[:k] are systematic data."""
|
| 38 |
+
orig = len(data)
|
| 39 |
+
L = (orig + k - 1) // k
|
| 40 |
+
d = data + bytes(L * k - orig)
|
| 41 |
+
shards = [bytearray(d[j * L:(j + 1) * L]) for j in range(k)]
|
| 42 |
+
C = cauchy(k, m)
|
| 43 |
+
for i in range(m):
|
| 44 |
+
P = bytearray(L)
|
| 45 |
+
Ci = C[i]
|
| 46 |
+
for j in range(k):
|
| 47 |
+
MC = MUL[Ci[j]]
|
| 48 |
+
Dj = shards[j]
|
| 49 |
+
for b in range(L):
|
| 50 |
+
P[b] ^= MC[Dj[b]]
|
| 51 |
+
shards.append(P)
|
| 52 |
+
return shards, L, orig
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _invert(M, k):
|
| 56 |
+
A = [list(M[i]) + [1 if j == i else 0 for j in range(k)] for i in range(k)]
|
| 57 |
+
for col in range(k):
|
| 58 |
+
piv = col
|
| 59 |
+
while A[piv][col] == 0:
|
| 60 |
+
piv += 1
|
| 61 |
+
A[col], A[piv] = A[piv], A[col]
|
| 62 |
+
inv = gf_inv(A[col][col])
|
| 63 |
+
A[col] = [MUL[inv][x] for x in A[col]]
|
| 64 |
+
for r in range(k):
|
| 65 |
+
if r != col and A[r][col]:
|
| 66 |
+
f = A[r][col]
|
| 67 |
+
A[r] = [A[r][x] ^ MUL[f][A[col][x]] for x in range(2 * k)]
|
| 68 |
+
return [A[i][k:] for i in range(k)]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def decode(present: dict, k: int, m: int, L: int, orig: int) -> bytes:
|
| 72 |
+
"""present: {shard_index: bytes}; needs >= k entries."""
|
| 73 |
+
idxs = sorted(present.keys())[:k]
|
| 74 |
+
C = cauchy(k, m)
|
| 75 |
+
Minv = _invert([_gen_row(r, k, C) for r in idxs], k)
|
| 76 |
+
data_shards = [bytearray(L) for _ in range(k)]
|
| 77 |
+
for j in range(k):
|
| 78 |
+
row = Minv[j]
|
| 79 |
+
Dj = data_shards[j]
|
| 80 |
+
for t, r in enumerate(idxs):
|
| 81 |
+
c = row[t]
|
| 82 |
+
if not c:
|
| 83 |
+
continue
|
| 84 |
+
MC = MUL[c]
|
| 85 |
+
src = present[r]
|
| 86 |
+
for b in range(L):
|
| 87 |
+
Dj[b] ^= MC[src[b]]
|
| 88 |
+
return b"".join(bytes(s) for s in data_shards)[:orig]
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def neural_parity_equiv(net_log, net_exp, k=4, m=2, L=16) -> bool:
|
| 92 |
+
"""Compute one parity shard with the NEURAL gf_mul and confirm it matches the
|
| 93 |
+
golden coding -- i.e. the verified units drive the erasure code bit-exactly."""
|
| 94 |
+
import os
|
| 95 |
+
from .gf256 import gf_mul_neural
|
| 96 |
+
data = os.urandom(L * k)
|
| 97 |
+
shards, _, _ = encode(data, k, m)
|
| 98 |
+
C = cauchy(k, m)
|
| 99 |
+
Dsh = [data[j * L:(j + 1) * L] for j in range(k)]
|
| 100 |
+
P = bytearray(L)
|
| 101 |
+
for j in range(k):
|
| 102 |
+
for b in range(L):
|
| 103 |
+
P[b] ^= gf_mul_neural(C[0][j], Dsh[j][b], net_log, net_exp)
|
| 104 |
+
return bytes(P) == bytes(shards[k]) # neural parity == golden parity
|
storage/scan.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Disc scanner.
|
| 3 |
+
|
| 4 |
+
Reads a source sector by sector into bytes. The source can be a raw optical
|
| 5 |
+
drive (Windows: r"\\\\.\\D:", may need admin) or an .iso / image file (for
|
| 6 |
+
testing). Unreadable sectors (scratches / disc rot) are recorded as BAD and
|
| 7 |
+
zero-filled -- ddrescue-style -- rather than aborting the scan.
|
| 8 |
+
|
| 9 |
+
Important: bad sectors are data you never read. RS self-healing (archive.py)
|
| 10 |
+
protects the STORED copy against future corruption; it cannot invent sectors the
|
| 11 |
+
drive could not read. To recover those, re-scan the disc and merge good sectors
|
| 12 |
+
across attempts (`merge_scans`).
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
SECTOR = 2048 # CD-ROM Mode-1 user data sector size
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def scan_image(path: str, sector: int = SECTOR):
|
| 20 |
+
"""-> (data: bytes, bad_sectors: list[int]). Robust to unreadable sectors."""
|
| 21 |
+
data = bytearray()
|
| 22 |
+
bad = []
|
| 23 |
+
with open(path, "rb", buffering=0) as f:
|
| 24 |
+
idx = 0
|
| 25 |
+
while True:
|
| 26 |
+
try:
|
| 27 |
+
blk = f.read(sector)
|
| 28 |
+
except OSError:
|
| 29 |
+
bad.append(idx)
|
| 30 |
+
data += bytes(sector)
|
| 31 |
+
try:
|
| 32 |
+
f.seek((idx + 1) * sector)
|
| 33 |
+
except OSError:
|
| 34 |
+
break
|
| 35 |
+
idx += 1
|
| 36 |
+
continue
|
| 37 |
+
if not blk:
|
| 38 |
+
break
|
| 39 |
+
data += blk # keep true length (last
|
| 40 |
+
idx += 1 # sector may be partial)
|
| 41 |
+
return bytes(data), bad
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def merge_scans(scans, sector: int = SECTOR):
|
| 45 |
+
"""Combine multiple (data, bad) scans of the same failing disc: a sector is
|
| 46 |
+
good if ANY scan read it. Returns (merged_data, still_bad_sectors)."""
|
| 47 |
+
n = max(len(d) // sector for d, _ in scans)
|
| 48 |
+
merged = bytearray(n * sector)
|
| 49 |
+
known = [False] * n
|
| 50 |
+
for data, bad in scans:
|
| 51 |
+
badset = set(bad)
|
| 52 |
+
for s in range(len(data) // sector):
|
| 53 |
+
if s not in badset and not known[s]:
|
| 54 |
+
merged[s * sector:(s + 1) * sector] = data[s * sector:(s + 1) * sector]
|
| 55 |
+
known[s] = True
|
| 56 |
+
still_bad = [s for s in range(n) if not known[s]]
|
| 57 |
+
return bytes(merged), still_bad
|
storage/vault.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Step 3 backing store: the Vault.
|
| 3 |
+
|
| 4 |
+
Combines everything so far into an on-disk repository:
|
| 5 |
+
* files are content-defined-chunked (Step 1 CDC)
|
| 6 |
+
* unique chunks are stored once, addressed by SHA-256 (dedup, bit-verified)
|
| 7 |
+
* each unique chunk is Reed-Solomon split into n = k+m shard files (Step 2),
|
| 8 |
+
so any k shards rebuild it -- up to m shard files can be lost/corrupted
|
| 9 |
+
|
| 10 |
+
On-disk layout:
|
| 11 |
+
<vault>/chunks/<sha256>/shard_0 .. shard_{n-1}, meta.json
|
| 12 |
+
<vault>/files/<relpath>.json (manifest: name, size, [chunk hashes])
|
| 13 |
+
|
| 14 |
+
Reads reconstruct on demand and RS-heal transparently; `heal()` regenerates any
|
| 15 |
+
missing shard files from the survivors. Nothing here beats entropy -- dedup
|
| 16 |
+
removes redundancy, RS adds redundancy for resilience.
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
import os
|
| 20 |
+
import json
|
| 21 |
+
import hashlib
|
| 22 |
+
from .chunkstore import chunk_stream
|
| 23 |
+
from . import rs
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Vault:
|
| 27 |
+
def __init__(self, path: str, k: int = 4, m: int = 2):
|
| 28 |
+
self.path = path
|
| 29 |
+
self.k, self.m = k, m
|
| 30 |
+
self.cdir = os.path.join(path, "chunks")
|
| 31 |
+
self.fdir = os.path.join(path, "files")
|
| 32 |
+
os.makedirs(self.cdir, exist_ok=True)
|
| 33 |
+
os.makedirs(self.fdir, exist_ok=True)
|
| 34 |
+
|
| 35 |
+
# ---- chunks ----
|
| 36 |
+
def _cd(self, h: str) -> str:
|
| 37 |
+
return os.path.join(self.cdir, h)
|
| 38 |
+
|
| 39 |
+
def _store_chunk(self, ch: bytes) -> str:
|
| 40 |
+
h = hashlib.sha256(ch).hexdigest()
|
| 41 |
+
d = self._cd(h)
|
| 42 |
+
if os.path.exists(os.path.join(d, "meta.json")):
|
| 43 |
+
return h # dedup: already stored
|
| 44 |
+
os.makedirs(d, exist_ok=True)
|
| 45 |
+
shards, L, orig = rs.encode(ch, self.k, self.m)
|
| 46 |
+
for i, s in enumerate(shards):
|
| 47 |
+
with open(os.path.join(d, f"shard_{i}"), "wb") as f:
|
| 48 |
+
f.write(bytes(s))
|
| 49 |
+
json.dump({"L": L, "orig": orig, "k": self.k, "m": self.m},
|
| 50 |
+
open(os.path.join(d, "meta.json"), "w"))
|
| 51 |
+
return h
|
| 52 |
+
|
| 53 |
+
def _load_chunk(self, h: str) -> bytes:
|
| 54 |
+
d = self._cd(h)
|
| 55 |
+
meta = json.load(open(os.path.join(d, "meta.json")))
|
| 56 |
+
n = meta["k"] + meta["m"]
|
| 57 |
+
present = {}
|
| 58 |
+
for i in range(n):
|
| 59 |
+
p = os.path.join(d, f"shard_{i}")
|
| 60 |
+
if os.path.exists(p):
|
| 61 |
+
b = open(p, "rb").read()
|
| 62 |
+
if len(b) == meta["L"]:
|
| 63 |
+
present[i] = b
|
| 64 |
+
if len(present) < meta["k"]:
|
| 65 |
+
raise IOError(f"chunk {h[:8]}: {len(present)} shards, need {meta['k']}")
|
| 66 |
+
ch = rs.decode(present, meta["k"], meta["m"], meta["L"], meta["orig"])
|
| 67 |
+
if hashlib.sha256(ch).hexdigest() != h:
|
| 68 |
+
raise IOError(f"chunk {h[:8]}: hash mismatch after decode")
|
| 69 |
+
return ch
|
| 70 |
+
|
| 71 |
+
# ---- files ----
|
| 72 |
+
def store_file(self, relpath: str, data: bytes) -> None:
|
| 73 |
+
chunks = [self._store_chunk(data[o:o + l]) for o, l in chunk_stream(data)]
|
| 74 |
+
mfp = os.path.join(self.fdir, relpath + ".json")
|
| 75 |
+
os.makedirs(os.path.dirname(mfp) or ".", exist_ok=True)
|
| 76 |
+
json.dump({"name": relpath, "size": len(data), "chunks": chunks}, open(mfp, "w"))
|
| 77 |
+
|
| 78 |
+
def store_folder(self, src: str) -> None:
|
| 79 |
+
for root, _, names in os.walk(src):
|
| 80 |
+
for nm in names:
|
| 81 |
+
fp = os.path.join(root, nm)
|
| 82 |
+
rel = os.path.relpath(fp, src).replace("\\", "/")
|
| 83 |
+
with open(fp, "rb") as f:
|
| 84 |
+
self.store_file(rel, f.read())
|
| 85 |
+
|
| 86 |
+
def list_files(self):
|
| 87 |
+
out = []
|
| 88 |
+
for root, _, names in os.walk(self.fdir):
|
| 89 |
+
for nm in names:
|
| 90 |
+
if nm.endswith(".json"):
|
| 91 |
+
mf = json.load(open(os.path.join(root, nm)))
|
| 92 |
+
out.append((mf["name"], mf["size"]))
|
| 93 |
+
return sorted(out)
|
| 94 |
+
|
| 95 |
+
def read_file(self, relpath: str) -> bytes:
|
| 96 |
+
mf = json.load(open(os.path.join(self.fdir, relpath + ".json")))
|
| 97 |
+
data = b"".join(self._load_chunk(h) for h in mf["chunks"])
|
| 98 |
+
return data[:mf["size"]]
|
| 99 |
+
|
| 100 |
+
def export(self, dst: str) -> int:
|
| 101 |
+
n = 0
|
| 102 |
+
for rel, _ in self.list_files():
|
| 103 |
+
data = self.read_file(rel)
|
| 104 |
+
op = os.path.join(dst, rel)
|
| 105 |
+
os.makedirs(os.path.dirname(op) or ".", exist_ok=True)
|
| 106 |
+
with open(op, "wb") as f:
|
| 107 |
+
f.write(data)
|
| 108 |
+
n += 1
|
| 109 |
+
return n
|
| 110 |
+
|
| 111 |
+
# ---- resilience ----
|
| 112 |
+
def heal(self) -> int:
|
| 113 |
+
healed = 0
|
| 114 |
+
for h in os.listdir(self.cdir):
|
| 115 |
+
d = self._cd(h)
|
| 116 |
+
mp = os.path.join(d, "meta.json")
|
| 117 |
+
if not os.path.isfile(mp):
|
| 118 |
+
continue
|
| 119 |
+
meta = json.load(open(mp))
|
| 120 |
+
n = meta["k"] + meta["m"]
|
| 121 |
+
missing = [i for i in range(n) if not os.path.exists(os.path.join(d, f"shard_{i}"))]
|
| 122 |
+
if missing:
|
| 123 |
+
ch = self._load_chunk(h) # decode from survivors
|
| 124 |
+
shards, _, _ = rs.encode(ch, meta["k"], meta["m"])
|
| 125 |
+
for i in missing:
|
| 126 |
+
with open(os.path.join(d, f"shard_{i}"), "wb") as f:
|
| 127 |
+
f.write(bytes(shards[i]))
|
| 128 |
+
healed += len(missing)
|
| 129 |
+
return healed
|
| 130 |
+
|
| 131 |
+
def integrity_ok(self) -> bool:
|
| 132 |
+
try:
|
| 133 |
+
for rel, _ in self.list_files():
|
| 134 |
+
self.read_file(rel)
|
| 135 |
+
return True
|
| 136 |
+
except IOError:
|
| 137 |
+
return False
|
| 138 |
+
|
| 139 |
+
def stats(self) -> dict:
|
| 140 |
+
logical = sum(sz for _, sz in self.list_files())
|
| 141 |
+
stored = 0
|
| 142 |
+
chunks = 0
|
| 143 |
+
for h in os.listdir(self.cdir):
|
| 144 |
+
d = self._cd(h)
|
| 145 |
+
if not os.path.isfile(os.path.join(d, "meta.json")):
|
| 146 |
+
continue
|
| 147 |
+
chunks += 1
|
| 148 |
+
for f in os.listdir(d):
|
| 149 |
+
if f.startswith("shard_"):
|
| 150 |
+
stored += os.path.getsize(os.path.join(d, f))
|
| 151 |
+
return {"logical": logical, "stored": stored, "unique_chunks": chunks,
|
| 152 |
+
"overhead": stored / max(1, logical)}
|
storage/vaultfs.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Read interface over a Vault, plus an optional WinFsp mount.
|
| 3 |
+
|
| 4 |
+
`VaultFS` is a dependency-free read model: list files, stat, and read byte ranges
|
| 5 |
+
that are reconstructed (and RS-healed) on demand from the Vault. This is what the
|
| 6 |
+
CLI `export` uses and what the mount serves.
|
| 7 |
+
|
| 8 |
+
`mount()` exposes the Vault as a real drive letter via WinFsp (Windows FUSE). It
|
| 9 |
+
is guarded: if `winfspy` / WinFsp is not installed, it raises with instructions
|
| 10 |
+
instead of failing obscurely. The dependency-free path (VaultFS + CLI export)
|
| 11 |
+
works everywhere; the live drive lights up once WinFsp is present.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
from collections import OrderedDict
|
| 15 |
+
from .vault import Vault
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class VaultFS:
|
| 19 |
+
def __init__(self, vault: Vault, cache_files: int = 8):
|
| 20 |
+
self.vault = vault
|
| 21 |
+
self._cache: OrderedDict[str, bytes] = OrderedDict()
|
| 22 |
+
self._cache_n = cache_files
|
| 23 |
+
self._index = {rel: sz for rel, sz in vault.list_files()}
|
| 24 |
+
|
| 25 |
+
def listdir(self):
|
| 26 |
+
return sorted(self._index.items()) # [(relpath, size)]
|
| 27 |
+
|
| 28 |
+
def size(self, relpath: str) -> int:
|
| 29 |
+
return self._index[relpath]
|
| 30 |
+
|
| 31 |
+
def _data(self, relpath: str) -> bytes:
|
| 32 |
+
if relpath in self._cache:
|
| 33 |
+
self._cache.move_to_end(relpath)
|
| 34 |
+
return self._cache[relpath]
|
| 35 |
+
data = self.vault.read_file(relpath) # reconstruct + RS-heal
|
| 36 |
+
self._cache[relpath] = data
|
| 37 |
+
if len(self._cache) > self._cache_n:
|
| 38 |
+
self._cache.popitem(last=False)
|
| 39 |
+
return data
|
| 40 |
+
|
| 41 |
+
def read(self, relpath: str, offset: int = 0, length: int | None = None) -> bytes:
|
| 42 |
+
data = self._data(relpath)
|
| 43 |
+
if length is None:
|
| 44 |
+
return data[offset:]
|
| 45 |
+
return data[offset:offset + length]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def winfsp_available() -> bool:
|
| 49 |
+
try:
|
| 50 |
+
import winfspy # noqa: F401
|
| 51 |
+
return True
|
| 52 |
+
except Exception:
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def mount(vault: Vault, mountpoint: str, label: str = "NeuralVault"):
|
| 57 |
+
"""Mount the vault as a read-only drive via WinFsp. Requires WinFsp + winfspy.
|
| 58 |
+
Experimental: the dependency-free `export` path is the tested one."""
|
| 59 |
+
if not winfsp_available():
|
| 60 |
+
raise RuntimeError(
|
| 61 |
+
"WinFsp / winfspy not found. Install WinFsp (https://winfsp.dev) and "
|
| 62 |
+
"`pip install winfspy`, or use the dependency-free CLI: "
|
| 63 |
+
"`python cli.py export <vault> <folder>`."
|
| 64 |
+
)
|
| 65 |
+
# Imported lazily so the module loads without winfspy present.
|
| 66 |
+
from winfspy import (FileSystem, BaseFileSystemOperations, NTStatusObjectNameNotFound,
|
| 67 |
+
FILE_ATTRIBUTE)
|
| 68 |
+
import time
|
| 69 |
+
|
| 70 |
+
fs_view = VaultFS(vault)
|
| 71 |
+
now = int(time.time() * 1e7) + 116444736000000000 # FILETIME epoch
|
| 72 |
+
|
| 73 |
+
class Ops(BaseFileSystemOperations):
|
| 74 |
+
def _info(self, is_dir, size=0):
|
| 75 |
+
return {
|
| 76 |
+
"file_attributes": FILE_ATTRIBUTE.FILE_ATTRIBUTE_DIRECTORY if is_dir
|
| 77 |
+
else FILE_ATTRIBUTE.FILE_ATTRIBUTE_NORMAL,
|
| 78 |
+
"allocation_size": size, "file_size": size,
|
| 79 |
+
"creation_time": now, "last_access_time": now,
|
| 80 |
+
"last_write_time": now, "change_time": now, "index_number": 0,
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
def _norm(self, p):
|
| 84 |
+
return p.replace("\\", "/").lstrip("/")
|
| 85 |
+
|
| 86 |
+
def get_volume_info(self):
|
| 87 |
+
return {"total_size": 0, "free_size": 0, "volume_label": label}
|
| 88 |
+
|
| 89 |
+
def get_security_by_name(self, file_name):
|
| 90 |
+
rel = self._norm(file_name)
|
| 91 |
+
is_dir = rel == "" or any(f.startswith(rel + "/") for f in fs_view._index)
|
| 92 |
+
if not is_dir and rel not in fs_view._index:
|
| 93 |
+
raise NTStatusObjectNameNotFound()
|
| 94 |
+
return (self._info(is_dir, 0 if is_dir else fs_view.size(rel))["file_attributes"], None, 0)
|
| 95 |
+
|
| 96 |
+
def open(self, file_name, create_options, granted_access):
|
| 97 |
+
rel = self._norm(file_name)
|
| 98 |
+
is_dir = rel == "" or any(f.startswith(rel + "/") for f in fs_view._index)
|
| 99 |
+
if not is_dir and rel not in fs_view._index:
|
| 100 |
+
raise NTStatusObjectNameNotFound()
|
| 101 |
+
return {"rel": rel, "is_dir": is_dir}
|
| 102 |
+
|
| 103 |
+
def get_file_info(self, fh):
|
| 104 |
+
return self._info(fh["is_dir"], 0 if fh["is_dir"] else fs_view.size(fh["rel"]))
|
| 105 |
+
|
| 106 |
+
def read(self, fh, offset, length):
|
| 107 |
+
return fs_view.read(fh["rel"], offset, length)
|
| 108 |
+
|
| 109 |
+
def read_directory(self, fh, marker):
|
| 110 |
+
prefix = "" if fh["rel"] == "" else fh["rel"] + "/"
|
| 111 |
+
names = set()
|
| 112 |
+
for f in fs_view._index:
|
| 113 |
+
if f.startswith(prefix):
|
| 114 |
+
rest = f[len(prefix):]
|
| 115 |
+
names.add(rest.split("/")[0])
|
| 116 |
+
out = []
|
| 117 |
+
for nm in sorted(names):
|
| 118 |
+
full = prefix + nm
|
| 119 |
+
is_dir = full not in fs_view._index
|
| 120 |
+
out.append({"file_name": nm,
|
| 121 |
+
"file_info": self._info(is_dir, 0 if is_dir else fs_view.size(full))})
|
| 122 |
+
return out
|
| 123 |
+
|
| 124 |
+
def close(self, fh):
|
| 125 |
+
pass
|
| 126 |
+
|
| 127 |
+
fs = FileSystem(mountpoint, Ops(), volume_label=label)
|
| 128 |
+
fs.start()
|
| 129 |
+
return fs
|