acquire_inputs.py
python · 179 lines · Sanitized derivative
Sanitized derivative: private paths or personal test identities use explicit placeholders. Original line count is preserved.
Version & provenance · data-cm7
Recorded SHA-256 999f67e7573c35b77d98b34495bda950809a1e2f18c56ff4bec45ed2c5877a74Rendered SHA-256 04764bb8bcc038d90d17c3b36c6fec5534047195ab9e9c140c0d23b199c82505
Apache-2.0 original project code; dependencies retain their own licenses. License notes. Line links are stable within this snapshot. CM-7 completed implementation source; original and public rendered hashes recorded. Host-specific paths use /work placeholders; inspect reproduction instructions before running. Separate Rust helper retains its GPL license.
1"""One-stream, bounded acquisition of the two books and complete small tables."""
2import argparse, base64, hashlib, json, os, re, shutil, time, urllib.request, zipfile
3from pathlib import Path
4from curriculum_policy import preflight, TASK_CAP, FREE_RESERVE
5
6ROOT = Path('/work/littleply-curriculum-20261029')
7LOCAL = Path(__file__).resolve().parent
8MIRROR = 'https://tablebase.lichess.ovh/tables/standard/'
9BOOKS = ['8moves_v3.pgn', 'bjbraams_chessdb_198350_lines.pgn']
10
11def sha(path):
12 h=hashlib.sha256()
13 with Path(path).open('rb') as f:
14 for block in iter(lambda:f.read(1<<20), b''): h.update(block)
15 return h.hexdigest()
16
17def allocation():
18 return sum(p.stat().st_size for root in (ROOT,LOCAL) for p in root.rglob('*') if p.is_file())
19
20def guard(extra=0):
21 if allocation()+extra > TASK_CAP: raise RuntimeError('2GB task cap')
22 if min(shutil.disk_usage(ROOT).free,shutil.disk_usage('/mnt/c').free)-extra < FREE_RESERVE:
23 raise RuntimeError('50GB reserve on both filesystems')
24
25def save(name,value):
26 data=json.dumps(value,indent=2).encode(); guard(len(data))
27 path=ROOT/name; temp=path.with_suffix(path.suffix+'.tmp')
28 temp.write_bytes(data); temp.replace(path)
29
30def small(url,name):
31 p=ROOT/name
32 if p.exists(): return p.read_bytes()
33 guard(2_000_000)
34 with urllib.request.urlopen(url,timeout=30) as r: data=r.read(2_000_001)
35 if len(data)>2_000_000: raise ValueError('metadata response oversized')
36 p.write_bytes(data)
37 return data
38
39def parse_listing(data,suffix):
40 found={}
41 for line in data.decode().splitlines():
42 match=re.search(r'<a href="(K[QRBNP]*vK[QRBNP]*\.'+suffix+r')">.*</a>\s+\S+\s+\S+\s+(\d+)\s*$',line)
43 if match:
44 name,size=match.groups()
45 if name in found: raise ValueError('duplicate listing')
46 found[name]=int(size)
47 if not found: raise ValueError('empty listing')
48 return found
49
50def parse_hashes(data):
51 found={}
52 for line in data.decode().splitlines():
53 h,n=line.split()
54 if not re.fullmatch('[0-9a-f]{64}',h): raise ValueError('bad SHA256')
55 if n in found and found[n]!=h: raise ValueError('conflicting SHA256')
56 found[n]=h
57 return found
58
59def book_sri(content):
60 normalized=content.replace(b'\r\n',b'\n').replace(b'\r',b'\n')
61 return base64.b64encode(hashlib.sha384(normalized).digest()).decode()
62
63def metadata():
64 ROOT.mkdir(exist_ok=True)
65 guard()
66 before={'host_free':shutil.disk_usage('/mnt/c').free,'native_free':shutil.disk_usage(ROOT).free,'task_bytes':allocation()}
67 revisions={}
68 for repo in ('official-stockfish/books','syzygy1/tb'):
69 revisions[repo]=json.loads(small('https://api.github.com/repos/'+repo+'/commits/master',repo.replace('/','-')+'-commit.json'))['sha']
70 br='https://raw.githubusercontent.com/official-stockfish/books/'+revisions['official-stockfish/books']+'/'
71 sr='https://raw.githubusercontent.com/syzygy1/tb/'+revisions['syzygy1/tb']+'/'
72 books=json.loads(small(br+'books.json','books.json'))
73 small(br+'LICENSE','books-LICENSE.txt')
74 small(br+'update_json.py','upstream-update_json.txt')
75 small(sr+'README.md','syzygy-README.txt')
76 small(sr+'src/checksum.c','upstream-checksum.txt')
77 hashes=parse_hashes(small(MIRROR+'sha256','mirror-sha256.txt'))
78 rows=[]
79 for kind,suffix in (('wdl','rtbw'),('dtz','rtbz')):
80 known=small(sr+'checksums/'+kind+'345.txt',kind+'345.txt').decode()
81 known=dict(line.split(': ') for line in known.splitlines())
82 listing=parse_listing(small(MIRROR+'3-4-5-'+kind+'/',kind+'-listing.html'),suffix)
83 if set(known)!=set(listing): raise ValueError('upstream/mirror filename set mismatch')
84 for name,size in sorted(listing.items()):
85 rows.append(dict(name=name,bytes=size,url=MIRROR+'3-4-5-'+kind+'/'+name,
86 sha256=hashes[name],upstream_embedded_checksum=known[name],kind='table'))
87 reusable={}
88 for r in rows:
89 p=ROOT/r['name']
90 if p.exists():
91 if p.stat().st_size!=r['bytes'] or sha(p)!=r['sha256']: raise ValueError('existing table mismatch')
92 reusable[r['name']]=r['bytes']
93 check=preflight(rows,host_free=before['host_free'],native_free=before['native_free'],
94 task_bytes=before['task_bytes'],reusable=reusable)
95 for name in BOOKS:
96 url=br+name+'.zip'
97 with urllib.request.urlopen(urllib.request.Request(url,method='HEAD'),timeout=30) as r:
98 size=int(r.headers['Content-Length'])
99 if not 0<size<25_000_000: raise ValueError('unexpected book size')
100 rows.append(dict(name=name+'.zip',bytes=size,url=url,kind='book',manifest=books[name]))
101 guard(sum(r['bytes'] for r in rows if not (ROOT/r['name']).exists())+100_000)
102 plan=dict(revisions=revisions,before=before,preflight=check,files=rows,
103 expected_table_bytes=sum(r['bytes'] for r in rows if r['kind']=='table'),
104 expected_book_bytes=sum(r['bytes'] for r in rows if r['kind']=='book'))
105 save('download-plan.json',plan)
106 print(json.dumps({k:v for k,v in plan.items() if k!='files'}),flush=True)
107
108def stream_file(response,path,expected_size,expected_sha=None,rate=8_000_000,check=None):
109 """No resume: retain partial as evidence; no overwriting an existing partial."""
110 partial=path.with_suffix(path.suffix+'.part')
111 h=hashlib.sha256(); n=0; start=time.monotonic()
112 with partial.open('xb') as f:
113 while True:
114 chunk=response.read(min(1<<20,expected_size-n+1))
115 if not chunk: break
116 if n+len(chunk)>expected_size: raise ValueError('response exceeds approved size')
117 if check: check(len(chunk))
118 f.write(chunk); h.update(chunk); n+=len(chunk)
119 delay=n/rate-(time.monotonic()-start)
120 if delay>0: time.sleep(min(delay,0.2))
121 f.flush(); os.fsync(f.fileno())
122 if n!=expected_size: raise ValueError('truncated response')
123 digest=h.hexdigest()
124 if expected_sha and digest!=expected_sha: raise ValueError('SHA256 mismatch')
125 partial.replace(path)
126 return digest
127
128def download():
129 plan=json.loads((ROOT/'download-plan.json').read_text())
130 # Re-evaluate whole plan and disk limits immediately before payload begins.
131 rows=plan['files']; tables=[r for r in rows if r['kind']=='table']
132 reused={}
133 for r in tables:
134 p=ROOT/r['name']
135 if p.exists():
136 if p.stat().st_size!=r['bytes'] or sha(p)!=r['sha256']: raise ValueError('reuse integrity')
137 reused[r['name']]=r['bytes']
138 preflight(tables,host_free=shutil.disk_usage('/mnt/c').free,native_free=shutil.disk_usage(ROOT).free,
139 task_bytes=allocation(),reusable=reused)
140 guard(sum(r['bytes'] for r in rows if not (ROOT/r['name']).exists())+100_000)
141 receipt=[]
142 for i,r in enumerate(rows):
143 p=ROOT/r['name']; reuse=p.exists()
144 if not reuse:
145 with urllib.request.urlopen(urllib.request.Request(r['url'],headers={'Accept-Encoding':'identity'}),timeout=60) as response:
146 if int(response.headers.get('Content-Length',-1))!=r['bytes']: raise ValueError('HTTP size changed')
147 stream_file(response,p,r['bytes'],r.get('sha256'),check=guard)
148 if p.stat().st_size!=r['bytes']: raise ValueError('local size mismatch')
149 digest=sha(p)
150 if r.get('sha256') and digest!=r['sha256']: raise ValueError('local hash mismatch')
151 entry={**r,'local_sha256':digest,'reused':reuse}
152 if r['kind']=='table':
153 if p.stat().st_size % 64 != 16: raise ValueError('no embedded checksum')
154 with p.open('rb') as f:
155 f.seek(-16,2); embedded=f.read(16).hex()
156 if embedded!=r['upstream_embedded_checksum']: raise ValueError('upstream embedded checksum mismatch')
157 entry['upstream_embedded_checksum_match']=True
158 if r['kind']=='book':
159 with zipfile.ZipFile(p) as z:
160 member=r['name'][:-4]
161 if z.namelist()!=[member]: raise ValueError('unexpected archive members')
162 if z.getinfo(member).file_size>100_000_000: raise ValueError('oversized expanded book')
163 sri=book_sri(z.read(member))
164 if sri!=r['manifest']['sri']: raise ValueError('uncompressed SHA384 SRI mismatch')
165 entry.update(sri_verified=True,uncompressed_bytes=z.getinfo(member).file_size)
166 receipt.append(entry)
167 if i%20==0 or i==len(rows)-1:
168 save('download-progress.json',dict(complete=len(receipt),total=len(rows),bytes=sum(x['bytes'] for x in receipt),last=p.name))
169 print(json.dumps(dict(complete=len(receipt),total=len(rows),last=p.name)),flush=True)
170 save('download-receipt.json',dict(files=receipt,table_bytes=plan['expected_table_bytes'],
171 book_bytes=plan['expected_book_bytes'],task_bytes=allocation(),before=plan['before'],
172 after={'host_free':shutil.disk_usage('/mnt/c').free,'native_free':shutil.disk_usage(ROOT).free},
173 complete=True,table_hash_verification='full file SHA256 matches mirror HTTPS manifest',
174 upstream_embedded_verification='all footers match pinned syzygy1/tb values; payload integrity independently verified by full-file mirror SHA256'))
175
176if __name__=='__main__':
177 p=argparse.ArgumentParser();p.add_argument('phase',choices=['metadata','download']);args=p.parse_args()
178 globals()[args.phase]()
179