"""Bounded local JPEG/PNG family inventory. Never a privacy or redaction certificate."""
from pathlib import Path
import hashlib,json,struct,sys,zlib
MAX=16*1024*1024
def png_parts(b):
 assert b.startswith(b'\x89PNG\r\n\x1a\n'),'not PNG';p=8;out=[];ended=False
 while p<len(b):
  assert p+12<=len(b),'truncated PNG header';n=int.from_bytes(b[p:p+4],'big');t=b[p+4:p+8];assert n<=MAX and p+12+n<=len(b),'invalid PNG length';data=b[p+8:p+8+n];assert len(out)<10000,'local chunk bound';assert(zlib.crc32(t+data)&0xffffffff)==int.from_bytes(b[p+8+n:p+12+n],'big'),'PNG CRC mismatch';assert all(65<=c<=90 or 97<=c<=122 for c in t),'invalid chunk type';out.append((t,data,p,b[p:p+n+12]));p+=n+12
  if t==b'IEND':assert n==0 and p==len(b),'data after IEND';ended=True;break
 assert ended and out[0][0]==b'IHDR'and len(out[0][1])==13,'PNG structure';assert sum(t==b'IHDR'for t,_,_,_ in out)==1;assert any(t==b'IDAT'for t,_,_,_ in out),'missing IDAT';types=[t for t,_,_,_ in out];assert types.count(b'eXIf')<=1,'duplicate eXIf';assert b'eXIf'not in types or types.index(b'eXIf')<types.index(b'IDAT'),'eXIf after IDAT';return out
def jpeg_parts(b):
 assert b[:2]==b'\xff\xd8','not JPEG';out=[];p=2;in_scan=False;scan_bytes=bytearray()
 while p<len(b):
  if in_scan:
   at=b.find(b'\xff',p);assert at>=0 and at+1<len(b),'truncated JPEG scan';scan_bytes.extend(b[p:at]);code=b[at+1]
   if code==0 or 0xd0<=code<=0xd7:scan_bytes.extend(b[at:at+2]);p=at+2;continue
   if code==0xff:p=at+1;continue
   p=at;in_scan=False
  assert p+2<=len(b)and b[p]==0xff,'JPEG marker expected';start=p
  while p+1<len(b)and b[p+1]==0xff:p+=1
  marker=b[p+1];p+=2
  if marker==0xd9:assert p==len(b),'data after EOI';return out,hashlib.sha256(scan_bytes).hexdigest()
  assert marker not in [0,0xd8]and not 0xd0<=marker<=0xd7,'unexpected standalone marker';assert p+2<=len(b),'truncated JPEG length';n=int.from_bytes(b[p:p+2],'big');assert n>=2 and p+n<=len(b),'invalid JPEG segment';data=b[p+2:p+n];assert len(out)<10000,'local segment bound';out.append((marker,data,start,b[start:p+n]));p+=n
  if marker==0xda:in_scan=True
 raise AssertionError('missing EOI')
def inspect(b):
 assert len(b)<=MAX,'outside local size bound';families=[];unknown=[];dimensions=None;parts=[];scan=None
 if b.startswith(b'\x89PNG\r\n\x1a\n'):
  fmt='png';chunks=png_parts(b);dimensions=list(struct.unpack('>II',chunks[0][1][:8]));known={b'IHDR',b'IDAT',b'IEND',b'PLTE',b'tRNS',b'gAMA',b'cHRM',b'sRGB',b'iCCP',b'pHYs',b'sBIT',b'bKGD'}
  for t,data,p,raw in chunks:
   name=t.decode('ascii');parts.append(name)
   if t in [b'eXIf',b'tEXt',b'iTXt',b'zTXt']:families.append({'family':name,'offset':p,'bytes':len(data)})
   elif t not in known:unknown.append(name)
  scan=hashlib.sha256(b''.join(data for t,data,_,_ in chunks if t==b'IDAT')).hexdigest()
 elif b.startswith(b'\xff\xd8'):
  fmt='jpeg';segments,scan=jpeg_parts(b)
  for marker,data,p,raw in segments:
   parts.append(hex(marker))
   if marker==0xe1:families.append({'family':'EXIF'if data.startswith(b'Exif\0\0')else'XMP'if data.startswith(b'http://ns.adobe.com/xap/1.0/\0')else'APP1-uninterpreted','offset':p,'bytes':len(data)})
   elif marker==0xfe:families.append({'family':'COM','offset':p,'bytes':len(data)})
   elif marker==0xed:families.append({'family':'APP13-uninterpreted','offset':p,'bytes':len(data)})
   elif 0xe0<=marker<=0xef and not(marker==0xe0 and data.startswith(b'JFIF\0')):unknown.append(hex(marker))
   elif marker in [0xc0,0xc1,0xc2]:assert len(data)>=5;dimensions=[int.from_bytes(data[3:5],'big'),int.from_bytes(data[1:3],'big')]
  assert dimensions is not None,'JPEG dimensions unavailable'
 else:raise AssertionError('outside JPEG/PNG scope')
 return {'format':fmt,'bytes':len(b),'sha256':hashlib.sha256(b).hexdigest(),'encodedDimensions':dimensions,'selectedMetadataFamilies':families,'uninterpretedContainers':unknown,'encodedImageDataSha256':scan,'containerParts':parts,'limit':'Family inventory only; no full field/value decoding, OCR, image redaction, rights, recipient or safety certification.'}
if __name__=='__main__':
 try:
  with Path(sys.argv[1]).open('rb')as f:b=f.read(MAX+1)
  r=inspect(b);print(json.dumps(r,indent=2))
 except(AssertionError,IndexError,OSError)as e:print(json.dumps({'status':'uninspected','reason':str(e),'noSafetyConclusion':True}));sys.exit(2)
