Files
twofauth-rs/tools/inventory.py
T
wt cf25e28c28
Rust verification / test (push) Successful in 4m10s
initial commit
2026-10-28 15:59:54 +07:00

69 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Extract source evidence, not a substitute for Laravel route:list / migrated schema."""
import argparse, hashlib, json, pathlib, re, subprocess
def quoted(s): return re.findall(r"'([^']*)'",s)
def middleware(s):
m=re.search(r"(?:'middleware'\s*=>|middleware\()\s*(\[[^]]*\]|'[^']*')",s)
return quoted(m[1]) if m else []
def routes(root,rel,prefix,base):
lines=(root/rel).read_text().splitlines(); stack=[]; result=[]; i=0
while i<len(lines):
s=lines[i].strip(); line=i+1
if s.startswith('//'): i+=1; continue
if s.startswith('Route::group(') or (s.startswith('Route::middleware(') and '->group(' in s):
stack.append(middleware(s)); i+=1; continue
if s=='});' and stack: stack.pop(); i+=1; continue
if s.startswith('Route::withoutMiddleware('):
while '->get(' not in s: i+=1; s+=' '+lines[i].strip()
# /up's excluded middleware are retained separately below.
if re.match(r'Route::(?:get|post|put|patch|delete|apiResource|withoutMiddleware)',s):
while ';' not in s: i+=1; s+=' '+lines[i].strip()
m=re.search(r"(?:Route::|->)(get|post|put|patch|delete|apiResource)\('([^']*)'",s)
if not m: raise ValueError(f'unparsed {rel}:{line}')
verb,path=m.groups(); inherited=base+sum(stack,[]); local=middleware(s) if verb!='get' or 'withoutMiddleware' not in s else []
handler=re.search(r'\[([A-Za-z]+)::class,\s*\'([^\']+)\'\]',s)
controller=re.search(r"apiResource\('[^']+',\s*(\w+)::class",s)
name=re.search(r"->name\('([^']+)'\)",s)
constraints=dict(re.findall(r"->where\('([^']+)',\s*'([^']+)'\)",s))
variants=[(verb.upper(),path,handler[1]+'@'+handler[2] if handler else 'closure',name[1] if name else None)]
if verb=='apiResource':
singular={'twofaccounts':'twofaccount','groups':'group','users':'user'}[path]
variants=[(v,path+suffix,controller[1]+'@'+action,path+'.'+action) for v,suffix,action in [('GET','','index'),('POST','','store'),('GET','/{'+singular+'}','show'),('PUT','/{'+singular+'}','update'),('PATCH','/{'+singular+'}','update'),('DELETE','/{'+singular+'}','destroy')] if not (path=='users' and action=='update')]
for method,p,h,n in variants:
entry=dict(method=method,path=prefix+'/'+p.lstrip('/'),handler=h,name=n,middleware_declared=inherited+local,constraints=constraints,source=f'{rel}:{line}')
if p=='/up': entry['excluded_middleware']=['StartSession','PreventRequestForgery','SubstituteBindings','SetLanguage','CustomCreateFreshApiToken']
if method=='GET': entry['implicit_HEAD']=True
result.append(entry)
i+=1
return result
def main():
p=argparse.ArgumentParser(); p.add_argument('--upstream',type=pathlib.Path,default=pathlib.Path('upstream')); p.add_argument('--out',type=pathlib.Path,default=pathlib.Path('rust-server/compatibility')); a=p.parse_args(); root=a.upstream; a.out.mkdir(parents=True,exist_ok=True)
if (root/'.git').exists():
commit=subprocess.check_output(['git','-C',str(root),'rev-parse','HEAD'],text=True).strip()
else:
commit=(root/'UPSTREAM_COMMIT').read_text().strip()
if not re.fullmatch(r'[0-9a-f]{40}',commit): raise ValueError('invalid pinned commit metadata')
kernel=(root/'app/Http/Kernel.php').read_text()
def raw_block(name):
m=re.search(r'protected \$'+name+r'\s*=\s*\[(.*?)\n \];',kernel,re.S)
return m[1].strip() if m else None
rt=routes(root,'routes/web.php','',['web'])+routes(root,'routes/api/v1.php','/api/v1',['api.v1'])
payload={'upstream_commit':commit,'kind':'static application declarations; not runtime package routes','notes':['GET routes implicitly support HEAD. Ordering is source ordering.','Middleware declarations require alias/group expansion, deduplication and Laravel priority sorting.','Package-discovered routes and OPTIONS/CORS behavior require runtime route:list.','settingName has global constraint [a-zA-Z]+ from RouteServiceProvider.'], 'middleware_source':'app/Http/Kernel.php','middleware_global_php':raw_block('middleware'),'middleware_groups_php':raw_block('middlewareGroups'),'middleware_aliases_php':raw_block('middlewareAliases'),'middleware_priority_php':raw_block('middlewarePriority'),'routes':rt}
write=lambda name,data:(a.out/name).write_text(json.dumps(data,indent=2)+'\n')
write('routes.json',payload)
migrations=[]
for f in sorted((root/'database/migrations').glob('*.php')):
src=f.read_text(); up=re.split(r'public function down\(',src)[0]
migrations.append({'file':str(f.relative_to(root)),'sha256':hashlib.sha256(src.encode()).hexdigest(),'schema_operations':re.findall(r"Schema::(create|table|rename|dropIfExists)\(\s*'([^']+)'",up),'up_source':up,'note':'Raw up source includes exact definitions and transformations. Helpers defined after down may need the full source; no SQL schema is inferred.'})
write('database.json',{'upstream_commit':commit,'kind':'ordered migration evidence; not a materialized SQL schema','drivers':['sqlite','mysql','pgsql','sqlsrv'],'migrations':migrations})
env={}
for f in sorted((root/'config').glob('*.php')):
for n,line in enumerate(f.read_text().splitlines(),1):
if line.lstrip().startswith('//'): continue
for key in re.findall(r"env\('([^']+)'",line): env.setdefault(key,[]).append(f'{f.relative_to(root)}:{n}')
write('environment.json',{'upstream_commit':commit,'kind':'configuration env names and source references only; no deployment values','variables':env,'dynamic_names':['USERPREF_DEFAULT__<PREFERENCE>','USERPREF_LOCKED__<PREFERENCE>']})
print(json.dumps({'commit':commit,'application_routes':len(rt),'migrations':len(migrations),'env_names':len(env)}))
if __name__=='__main__': main()