"""Named upstream recipes; bounded verified-TLS bodies, strict MIST member ranges. --cache may be repeated for an entirely offline copy/validation replay. No temporary generated URL is used as a permanent source. No server upload. """ from pathlib import Path import argparse,hashlib,io,json,re,shutil,struct,subprocess,urllib.parse,zipfile,zlib from model_io import RECIPES,verify_inputs parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--out',type=Path,default=Path('inputs'));parser.add_argument('--cache',type=Path,action='append',default=[]) args=parser.parse_args();args.out.mkdir(parents=True,exist_ok=True);ledger=[];maximum=15000000 def request(name,url,cap,form=None,method='GET',byte_range=None): if sum(row['bytes'] for row in ledger)+cap>maximum:raise ValueError('Request cap exceeds15MBresponse-body budget') path=args.out/name if path.exists():raise FileExistsError('Retain previous body; use a fresh output directory') headers=args.out/(name+'.headers') command=['curl','--silent','--show-error','--max-redirs','0','--max-time','120','--proto','=https','--header','Accept-Encoding: identity','--dump-header',str(headers),'--output',str(path),'--write-out','%{json}',url] if method=='HEAD':command.append('--head') else:command+=['--max-filesize',str(cap)] if byte_range is not None:command+=['--range',byte_range] payload=urllib.parse.urlencode(form).encode() if form is not None else None if payload is not None:command+=['--data-binary','@-'] run=subprocess.run(command,input=payload,capture_output=True);meta=json.loads(run.stdout or '{}') if method=='HEAD':path.write_bytes(b'') body=path.read_bytes() if path.exists() else b'' blocks=headers.read_text().strip().split('\n\n');block=next(value for value in reversed(blocks) if value.startswith('HTTP/')) header={line.split(':',1)[0].lower():line.split(':',1)[1].strip() for line in block.splitlines()[1:] if ':' in line} ledger.append({'name':name,'url':url,'form':form,'method':'POST' if form is not None else method,'range':byte_range,'status':meta.get('http_code'),'bytes':len(body),'sha256':hashlib.sha256(body).hexdigest(),'curlExit':run.returncode}) (args.out/'fetch-receipt.json').write_text(json.dumps({'responses':ledger,'actualBodyBytes':sum(row['bytes'] for row in ledger)},indent=2)+'\n') if run.returncode or meta.get('ssl_verify_result')!=0 or meta.get('http_code')!=(206 if byte_range else 200) or len(body)>cap:raise ValueError('Source/TLS/range/cap refused; no automatic retry or full-archive fallback') if byte_range: value=header.get('content-range','');match=re.fullmatch(r'bytes (\d+)-(\d+)/(\d+)',value) if not match:raise ValueError('Missing exact Content-Range') start,end,total=map(int,match.groups()) if end-start+1!=len(body) or (byte_range.startswith('-') and (end!=total-1 or len(body)!=min(int(byte_range[1:]),total))) or (not byte_range.startswith('-') and byte_range!=f'{start}-{end}'):raise ValueError('Range/body mismatch') return path,header required=[row['name'] for row in RECIPES['direct']]+[f'parsec-dense-av{av:.1f}.dat' for av in [0,.1,.2]]+[RECIPES['mistPhotometryName']] if args.cache: for name in required: source=next((folder/name for folder in args.cache if (folder/name).is_file()),None) if source is None:raise FileNotFoundError('Offline cache missing '+name) shutil.copyfile(source,args.out/name) else: for source in RECIPES['direct']:request(source['name'],source['url'],max(source['bytes']+200000,500000),source.get('form')) for recipe in RECIPES['parsec']: av=recipe['av'];reply,_=request(f'parsec-av{av}-request.html','https://stev.oapd.inaf.it/cgi-bin/cmd_3.8',150000,recipe['form']) match=re.search(r'href="?([^\s"<>]*output[^\s"<>]*\.dat)',reply.read_text()) if not match:raise ValueError('CMDdid not return the requested table') url=urllib.parse.urljoin('https://stev.oapd.inaf.it/cgi-bin/cmd_3.8',match.group(1)) if not url.startswith('https://stev.oapd.inaf.it/tmp/'):raise ValueError('UnexpectedCMDsource') request(f'parsec-dense-av{av:.1f}.dat',url,700000) reply,_=request('mist-request.html','https://mist.science/iso_form.php',200000,RECIPES['mistRequest']) match=re.search(r'href="([^"]+\.zip)"',reply.read_text()) if not match:raise ValueError('NoMISTgenerated archive') url=urllib.parse.urljoin('https://mist.science/iso_form.php',match.group(1)) if not url.startswith('https://mist.science/output/'):raise ValueError('UnexpectedMISTsource') _,head=request('mist-head',url,0,method='HEAD');archive=int(head.get('content-length','0')) if head.get('accept-ranges')!='bytes' or archive<1:raise ValueError('Source has no declared exact byte ranges') tail,tail_headers=request('mist-central-directory.range',url,65557,byte_range='-65557');tail_start=archive-tail.stat().st_size if int(tail_headers['content-range'].split('/')[-1])!=archive:raise ValueError('Archive length changed after HEAD') with zipfile.ZipFile(io.BytesIO(tail.read_bytes())) as zipped: members=[member for member in zipped.infolist() if member.filename.endswith('.UBVRIplus')] if len(members)!=1:raise ValueError('Expected one combined photometry member') member=members[0];offset=member.header_offset+tail_start if member.file_size>=25000000 or member.compress_type!=zipfile.ZIP_DEFLATED or member.flag_bits&1:raise ValueError('Unsupported member') header,local_headers=request('mist-local-header.range',url,1024,byte_range=f'{offset}-{offset+1023}') if int(local_headers['content-range'].split('/')[-1])!=archive:raise ValueError('Archive length changed before local header') values=struct.unpack('<4s5H3I2H',header.read_bytes()[:30]);filename_length,extra_length=values[-2:] if values[0]!=b'PK\x03\x04' or header.read_bytes()[30:30+filename_length].decode()!=member.filename:raise ValueError('Local/central header mismatch') start=offset+30+filename_length+extra_length data,data_headers=request('mist-photometry-deflate.range',url,member.compress_size,byte_range=f'{start}-{start+member.compress_size-1}') if int(data_headers['content-range'].split('/')[-1])!=archive:raise ValueError('Archive length changed before member') decoded=zlib.decompress(data.read_bytes(),-15) if len(decoded)!=member.file_size or zlib.crc32(decoded)!=member.CRC:raise ValueError('Native member CRC/length mismatch') (args.out/RECIPES['mistPhotometryName']).write_bytes(decoded) receipts=verify_inputs(args.out) (args.out/'validated-inputs.json').write_text(json.dumps({'mode':'offline named cached bodies' if args.cache else 'fresh named upstream recipes','newHttpBodyBytes':sum(row['bytes'] for row in ledger),'inputs':receipts,'permission':'Source access is not a redistribution grant; all original/local derived rows remain on the reader computer.'},indent=2)+'\n') print('PASS actual source validation; network body bytes',sum(row['bytes'] for row in ledger))