69 lines
5.8 KiB
Python
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()
|