#!/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 igroup(' 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__','USERPREF_LOCKED__']}) print(json.dumps({'commit':commit,'application_routes':len(rt),'migrations':len(migrations),'env_names':len(env)})) if __name__=='__main__': main()