1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381(** Generate verified C libraries from Wire codecs via EverParse. *)openWire.Everparseletis_upperc=Char.uppercase_asciic=c&&Char.lowercase_asciic<>c(* Apply EverParse's normalization to a single identifier segment (no
underscores): every run of two or more consecutive uppercase letters keeps
its first letter and lowercases the rest, wherever it occurs in the segment
([SSID -> Ssid], [TMFrame -> Tmframe], [SpaceOSFrame -> SpaceOsframe]); a lone
uppercase letter (a camelCase word boundary) is left alone. *)letnormalize_segmentseg=letlen=String.lengthseginletb=Buffer.createleninleti=ref0inwhile!i<lendoifis_upperseg.[!i]thenbeginletj=ref!iinwhile!j<len&&is_upperseg.[!j]doincrjdone;Buffer.add_charbseg.[!i];if!j-!i>=2thenfork=!i+1to!j-1doBuffer.add_charb(Char.lowercase_asciiseg.[k])done;i:=!jendelsebeginBuffer.add_charbseg.[!i];incrienddone;Buffer.contentsb(* EverParse strips underscores when generating a C identifier from a name and
joins the [_]-separated segments into CamelCase: each segment is capitalized
and any internal run of two or more uppercase letters collapses to one.
[Grpc_message -> GrpcMessage], [EP_Header -> EpHeader],
[MC_Status_Reply -> McStatusReply], [SSID -> Ssid]. wire writes the .3d file
and type with a capitalized name, so the leading segment is normally already
capitalized; capitalizing it here keeps the identifier matching EverParse even
for an otherwise lower-case name. *)leteverparse_namename=String.split_on_char'_'name|>List.map(funseg->String.capitalize_ascii(normalize_segmentseg))|>String.concat""(* EverParse's own identifier mangling, transcribed from [pascal_case] in
[src/3d/Target.fst]. It drops underscores and CamelCases, but unlike
{!everparse_name} it lowercases every character that follows an uppercase
letter *or a digit* until the next lower-case letter (a digit counts as
uppercase, since [uppercase '2' = '2']). So [TPM2B -> Tpm2b] and
[Foo2Bar -> Foo2bar], not [Tpm2B] / [Foo2Bar]. EverParse builds the wrapper
symbol as [pascal_case (module ^ "_check_" ^ codec)], so this must match it
exactly or the differential [agree.c] links against a symbol that does not
exist. *)letpascal_casename=ifnot(String.containsname'_')thenString.capitalize_asciinameelsebeginletkeep=0andup=1andlow=2inletwhat_next=refupinletb=Buffer.create(String.lengthname)inString.iter(func->ifc='_'thenwhat_next:=upelsebeginif!what_next=keepthenBuffer.add_charbcelseif!what_next=upthenBuffer.add_charb(Char.uppercase_asciic)elseBuffer.add_charb(Char.lowercase_asciic);ifChar.uppercase_asciic=cthenwhat_next:=lowelseifChar.lowercase_asciic=cthenwhat_next:=keepend)name;Buffer.contentsbend(* EverParse derives the C output filename from the [.3d] filename, which
[Wire.Everparse.filename] already writes with [String.capitalize_ascii].
All filenames wire.3d emits or references must go through the same
capitalization so dune targets match the files EverParse actually
produces. Identifiers inside C code -- the [<Name>Set*] setters and the
typed struct name -- use [everparse_name], which also strips underscores
and CamelCases segments ([rpmsg_endpoint_info] -> [RpmsgEndpointInfo]).
Keep the two concerns separate: [file_base] for filenames, [c_ident] for
C identifiers. *)letfile_base(s:t)=String.capitalize_asciis.nameletc_ident(s:t)=everparse_names.name(* EverParse normalizes extern callback names in ways that are awkward to
mirror exactly (runs of uppercase after a digit get lowercased, trailing
uppercase runs get lowercased, ...). Rather than re-implement EverParse's
rule and drift from it over time, we read the normalized names straight
out of the [_ExternalAPI.h] file EverParse has just generated. *)letread_extern_names~outdirs=letpath=Filename.concatoutdir(file_bases^"_ExternalAPI.h")inletic=open_inpathinletnames=ref[]in(trywhiletruedoletline=input_lineicinmatch(String.index_optline'(',String.index_optline' ',String.lengthline)with|Somelp,_,_whenString.lengthline>=11->letprefix="extern void "inletplen=String.lengthprefixinifString.lengthline>plen&&String.subline0plen=prefix&&lp>plenthenletname=String.sublineplen(lp-plen)innames:=name::!names|_->()donewithEnd_of_file->());close_inic;List.rev!names(* EverParse's top-level validator function follows its own normalization
rule (different from extern callbacks: it preserves underscores when the
name doesn't start with 2+ uppercase, strips them when it does). Rather
than duplicate EverParse's logic, extract the actual name from the
generated [<Name>.h]: [uint64_t <Name>Validate<Name>(...)]. *)letread_validate_name~outdirs=letpath=Filename.concatoutdir(file_bases^".h")inletic=open_inpathinletfound=refNoneinletneedle="Validate"inletnlen=String.lengthneedleinletis_identc=(c>='A'&&c<='Z')||(c>='a'&&c<='z')||(c>='0'&&c<='9')||c='_'in(* EverParse declares the validator as [<Name>Validate<Name>(...)]. Find the
[Validate] keyword and return the C identifier immediately before it -- the
base [<Name>]. Scanning every position (not just the first 'V') lets the
name itself contain a 'V' (e.g. "VeritySuperblock"); anchoring on the
identifier before [Validate] also drops a leading return type should
EverParse ever emit it on the same line. *)letbase_before_validateline=letlen=String.lengthlineinletrecscani=ifi+nlen>lenthenNoneelseifi>0&&is_identline.[i-1]&&String.sublineinlen=needlethenbeginletj=refiinwhile!j>0&&is_identline.[!j-1]dodecrjdone;Some(String.subline!j(i-!j))endelsescan(i+1)inscan0in(trywhile!found=Nonedoletline=String.trim(input_lineic)infound:=base_before_validatelinedonewithEnd_of_file->());close_inic;match!foundwith|Somen->n|None->Fmt.failwith"could not find Validate function name in %s"pathletwrite_3d~outdirschemas=Wire.Everparse.write~mode:`Ffi~outdirschemasletlocate_3d_exe()=letic=Unix.open_process_in"command -v 3d.exe 2>/dev/null"inletpath=trySome(input_lineic)withEnd_of_file->Noneinignore(Unix.close_process_inic);matchpathwith|Somep->Somep|None->letlocal=Filename.concat(Sys.getenv"HOME")".local/everparse/bin/3d.exe"inifSys.file_existslocalthenSomelocalelseNoneleteverparse_dir()=matchlocate_3d_exe()with|Someexe->Filename.dirnameexe|>Filename.dirname|None->failwith"3d.exe not found"(* A freestanding endianness branch for EverParseEndianness.h. The shipped
header keys byte order off an OS (<endian.h> on Linux, OSSwap on macOS, ...)
and [#error]s when none matches, so it does not compile for a no-OS target
like a unikernel (which defines neither [__linux__] nor [__APPLE__]). Any GCC
or Clang exposes byte order as [__BYTE_ORDER__] and byte-swaps with
[__builtin_bswap*] without an OS, so this branch, spliced in before the
[#error], makes the header portable to a cross target. *)letendianness_freestanding_branch={|#elif (defined(__GNUC__) || defined(__clang__)) && defined(__BYTE_ORDER__)
/* Freestanding target with no OS <endian.h> (e.g. a unikernel): take byte
order from the compiler and byte-swap with its builtins. */
# if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
# define htobe16(x) __builtin_bswap16(x)
# define htole16(x) (x)
# define be16toh(x) __builtin_bswap16(x)
# define le16toh(x) (x)
# define htobe32(x) __builtin_bswap32(x)
# define htole32(x) (x)
# define be32toh(x) __builtin_bswap32(x)
# define le32toh(x) (x)
# define htobe64(x) __builtin_bswap64(x)
# define htole64(x) (x)
# define be64toh(x) __builtin_bswap64(x)
# define le64toh(x) (x)
# else
# define htobe16(x) (x)
# define htole16(x) __builtin_bswap16(x)
# define be16toh(x) (x)
# define le16toh(x) __builtin_bswap16(x)
# define htobe32(x) (x)
# define htole32(x) __builtin_bswap32(x)
# define be32toh(x) (x)
# define le32toh(x) __builtin_bswap32(x)
# define htobe64(x) (x)
# define htole64(x) __builtin_bswap64(x)
# define be64toh(x) (x)
# define le64toh(x) __builtin_bswap64(x)
# endif
|}(* The EverParse anchor the freestanding branch is spliced before: the fallthrough
that rejects an unrecognized platform. *)letendianness_unsupported_anchor="#else\n\n#error \"Unsupported platform\""letwith_freestanding_endiannesscontent=letanchor=Re.compile(Re.strendianness_unsupported_anchor)inRe.replace_string~all:falseanchor~by:(endianness_freestanding_branch^endianness_unsupported_anchor)contentletcopy_everparse_endianness~outdir=letdst=Filename.concatoutdir"EverParseEndianness.h"inifnot(Sys.file_existsdst)thenbeginletep_dir=everparse_dir()inletsrc=Filename.concatep_dir"src/3d/EverParseEndianness.h"inifnot(Sys.file_existssrc)thenFmt.failwith"Cannot find EverParseEndianness.h at %s"src;letic=open_in_binsrcinletn=in_channel_lengthicinletbuf=really_input_stringicninclose_inic;letoc=open_out_bindstinoutput_stringoc(with_freestanding_endiannessbuf);close_outocendlethas_3d_exe()=locate_3d_exe()<>None(* The [_ExternalTypedefs.h] header seen by the EverParse-generated validator
and wrapper. The default shipped by wire.3d is a forward declaration that
ties WIRECTX to the matching [<Name>_Fields] plug struct. Users who want
their own plug (e.g. {!Wire_stubs} for OCaml FFI) overwrite this file with
a different WIRECTX definition; they must then also omit the default
[<Name>_Fields.c] from their link. *)letwrite_external_typedefs~outdirschemas=List.iter(funs->ifWire.Everparse.uses_wire_ctxsthenbeginletpath=Filename.concatoutdir(file_bases^"_ExternalTypedefs.h")inletoc=open_outpathinFmt.pf(Format.formatter_of_out_channeloc)"#ifndef WIRECTX_DEFINED@\n\
#define WIRECTX_DEFINED@\n\
typedef struct %sFields WIRECTX;@\n\
#endif@\n"(c_idents);close_outocend)schemas(* Typed-struct plug: one C struct per schema with one member per named field,
plus a [WireSet*] implementation that switches on idx to populate it. *)letwrite_fields_header~outdirs=letfields=Wire.Everparse.plug_fieldssinletbase=file_basesinletident=c_identsinletpath=Filename.concatoutdir(base^"_Fields.h")inletoc=open_outpathinletppf=Format.formatter_of_out_channelocinletprfmt=Fmt.pfppffmtinletguard=String.uppercase_asciiident^"_FIELDS_H"|>fung->String.map(func->ifc='-'then'_'elsec)ginletprefix=String.uppercase_asciiident|>funp->String.map(func->ifc='-'then'_'elsec)pinpr"#ifndef %s@\n"guard;pr"#define %s@\n"guard;pr"#include <stdint.h>@\n@\n";pr"/* Field indices -- use with the schema's WireSet* callbacks in a@\n";pr" custom [WIRECTX] if you only want to capture a subset. */@\n";List.iter(funf->pr"#define %s_IDX_%s %d@\n"prefix(String.uppercase_asciif.Wire.Everparse.name)f.idx)fields;iffields<>[]thenpr"@\n";pr"/* Default plug: one typed member per named field. Pass a pointer to@\n";pr" [%sFields] as [WIRECTX *] when you want every field populated. */@\n"ident;pr"typedef struct %sFields {@\n"ident;List.iter(funf->pr" %s %s;@\n"f.Wire.Everparse.c_typef.name)fields;iffields=[]thenpr" int _unused;@\n";pr"} %sFields;@\n"ident;pr"#endif@\n";Format.pp_print_flushppf();close_outoc(* Emit one [case N:] body inside a [WireSet*] setter. [float] / [double]
fields get a [memcpy] bit-reinterpret because the parser hands us the
underlying [UINT32] / [UINT64] but the plug struct stores the typed
float; everyone else takes a value cast. *)letemit_setter_caseppflogicalf=ifString.equalf.Wire.Everparse.setterlogicalthenmatchf.c_typewith|"float"|"double"->Fmt.pfppf" case %d: { %s _x; memcpy(&_x, &v, sizeof _x); f->%s = _x; \
break; }@\n"f.idxf.c_typef.name|_->Fmt.pfppf" case %d: f->%s = (%s) v; break;@\n"f.idxf.namef.c_typeletwrite_fields_impl~outdirs=letfields=Wire.Everparse.plug_fieldssinletsetters=Wire.Everparse.plug_setterssinletbase=file_basesinletident=c_identsin(* EverParse renames some setter symbols when emitting [.c] (for example,
uppercase runs after a digit are lowercased). Read the actual symbol
names from the just-generated [_ExternalAPI.h] rather than re-deriving
them. Order matches the declaration order of the extern functions,
which matches [plug_setters]. *)letphysical_names=read_extern_names~outdirsinletpath=Filename.concatoutdir(base^"_Fields.c")inletoc=open_outpathinletppf=Format.formatter_of_out_channelocinletprfmt=Fmt.pfppffmtinpr"#include <stdint.h>@\n";pr"#include <string.h>@\n";pr"#include \"%s_Fields.h\"@\n"base;pr"#include \"%s_ExternalTypedefs.h\"@\n"base;pr"#include \"%s_ExternalAPI.h\"@\n@\n"base;(* Cast [WIRECTX *] to the schema's concrete struct type. In a translation
unit that includes multiple schemas' [_Fields.c] files, only the first
[_ExternalTypedefs.h] defines [WIRECTX]; subsequent headers are skipped
by the include guard. The explicit cast to [<Name>Fields *] makes each
setter work regardless of which schema's typedef won. *)List.iter2(fun(logical,val_c_type)physical->pr"void %s(WIRECTX *ctx, uint32_t idx, %s v) {@\n"physicalval_c_type;pr" %sFields *f = (%sFields *) ctx;@\n"identident;pr" switch (idx) {@\n";List.iter(funf->emit_setter_caseppflogicalf)fields;pr" default: (void) f; (void) v; break;@\n";pr" }@\n";pr"}@\n@\n")settersphysical_names;Format.pp_print_flushppf();close_outocletwrite_fields~outdirschemas=List.iter(funs->ifWire.Everparse.uses_wire_ctxsthenbeginwrite_fields_header~outdirs;write_fields_impl~outdirsend)schemas(* Files shipped with a schema whose validator depends on the WireCtx contract:
the forward-decl header, the EverParse-emitted API / wrapper, and the
default plug pair. Every file in this list is installed and accounted for
in the dune rules; wrapper artefacts are only needed at install time,
while [_Fields.{c,h}] also link into the runtest. *)letwire_ctx_filesschemas=List.concat_map(funs->ifWire.Everparse.uses_wire_ctxsthenletbase=file_basesin[base^"_ExternalTypedefs.h";base^"_ExternalAPI.h";base^"Wrapper.c";base^"Wrapper.h";base^"_Fields.h";base^"_Fields.c";]else[])schemasletfields_c_filesschemas=List.filter_map(funs->ifWire.Everparse.uses_wire_ctxsthenSome(file_bases^"_Fields.c")elseNone)schemas(* EverParse's [<Base>Check<Codec>] wrapper returns TRUE on any successful
validator result, but a success result is the consumed position: a valid
record followed by trailing bytes still returns TRUE, making the wrapper a
prefix recognizer rather than a validator. Wire's contract is whole-buffer
validation, so rewrite each success tail to also require full consumption.
Textual on EverParse's wrapper shape (the tail is identical for
parameterized and plain entrypoints, [print_c_entry] in the upstream
[src/3d/Target.fst]): if a future EverParse emits neither the known tail
nor its own consumption check, fail loudly rather than silently shipping a
prefix recognizer. The behavioural backstop is the differential runtest,
whose corpus includes over-length inputs the oracle rejects. *)letwrapper_success_tail="\t\treturn FALSE;\n\t}\n\treturn TRUE;\n}"letwrapper_consumption_check="result != (uint64_t) len"letwrapper_hardened_tail="\t\treturn FALSE;\n\
\t}\n\
\tif (result != (uint64_t) len)\n\
\t{\n\
\t\treturn FALSE;\n\
\t}\n\
\treturn TRUE;\n\
}"letharden_wrapper~outdirbase=letpath=Filename.concatoutdir(base^"Wrapper.c")inifSys.file_existspaththenbeginletsrc=In_channel.with_open_textpathIn_channel.input_allinlettail=Re.compile(Re.strwrapper_success_tail)inifRe.execptailsrcthenOut_channel.with_open_textpath(funoc->Out_channel.output_stringoc(Re.replace_stringtail~by:wrapper_hardened_tailsrc))elseifnot(Re.execp(Re.compile(Re.strwrapper_consumption_check))src)thenFmt.failwith"%s: unrecognized EverParse wrapper shape; cannot insert the \
full-consumption check"pathendletrun_everparse_files?(quiet=true)~outdirfiles=letexe=matchlocate_3d_exe()with|Somee->e|None->failwith"3d.exe not found in PATH or ~/.local/everparse/bin/"inList.iter(funf->letredirect=ifquietthen" > /dev/null 2>&1"else""inletcmd=Fmt.str"cd %s && %s --batch %s%s"outdirexefredirectinletret=Sys.commandcmdinifret<>0thenFmt.failwith"EverParse failed on %s with code %d"fret;harden_wrapper~outdir(Filename.remove_extension(Filename.basenamef)))files;copy_everparse_endianness~outdirletrun_everparse?(quiet=true)~outdirschemas=run_everparse_files~quiet~outdir(List.mapWire.Everparse.filenameschemas)letparse_3d?(batch=false)~outdirfile=letexe=matchlocate_3d_exe()with|Somee->e|None->failwith"3d.exe not found in PATH or ~/.local/everparse/bin/"inletlog_path=Filename.temp_file"wire_parse_3d"".log"in(* 3d.exe emits diagnostics to stdout, so capture both streams. *)letflag=ifbatchthen"--batch "else""inletcmd=Fmt.str"cd %s && %s %s%s > %s 2>&1"outdirexeflagfilelog_pathinletret=Sys.commandcmdinletcaptured=tryIn_channel.with_open_textlog_pathIn_channel.input_allwithSys_error_->""in(trySys.removelog_pathwithSys_error_->());ifret=0thenOk()elseletmsg=String.split_on_char'\n'captured|>List.filter(funl->letl=String.trimlinl<>""&¬(String.lengthl>=11&&String.subl011="Processing "))|>String.concat"\n"inError(ifmsg=""thenFmt.str"exit %d"retelsemsg)letemit_sanity_checkppf~name~ep~ctx_argwire_size=letprfmt=Fmt.pfppffmtin(* Sanity: the OCaml codec's wire_size must match what the EverParse
validator consumes. A mismatch means the .3d projection of the codec
packs to a different size than the codec declares -- almost always a
bug in the codec's bitfield declarations. Later checks are meaningless
if this fails, so abort the whole test binary with a clear message. *)pr" r = %sValidate%s(%sNULL, counting_error_handler, buf, %d, 0);\n"epepctx_argwire_size;pr" if (!EverParseIsSuccess(r) || r != %d) {\n"wire_size;pr" fprintf(stderr,\n";pr" \"FATAL: %s wire_size mismatch -- codec declared %d bytes, \"\n"namewire_size;pr" \"EverParse validator returned %%llu. Fix the OCaml codec's \"\n";pr" \"wire_size or the .3d projection.\\n\",\n";pr" (unsigned long long) r);\n";pr" return 2;\n";pr" }\n"letemit_truncation_checksppf~ep~ctx_argwire_size=letprfmt=Fmt.pfppffmtinpr" r = %sValidate%s(%sNULL, counting_error_handler, buf, %d, 0);\n"epepctx_arg(wire_size*2);pr" CHECK(\"larger buffer validates\", EverParseIsSuccess(r));\n";pr" CHECK(\"position is %d not %d\", r == %d);\n"wire_size(wire_size*2)wire_size;pr"\n";pr" for (uint64_t len = 0; len < %d; len++) {\n"wire_size;pr" error_count = 0;\n";pr" r = %sValidate%s(%sNULL, counting_error_handler, buf, len, 0);\n"epepctx_arg;pr" CHECK(\"truncated to len fails\", EverParseIsError(r));\n";pr" }\n";pr"\n";pr" r = %sValidate%s(%sNULL, counting_error_handler, buf, 0, 0);\n"epepctx_arg;pr" CHECK(\"empty input fails\", EverParseIsError(r));\n"letemit_random_checksppf~ep~ctx_argwire_size=letprfmt=Fmt.pfppffmtinpr" srand(42);\n";pr" for (int i = 0; i < 1000; i++) {\n";pr" for (int j = 0; j < %d; j++)\n"wire_size;pr" buf[j] = (uint8_t)(rand() & 0xff);\n";pr" r = %sValidate%s(%sNULL, counting_error_handler, buf, %d, 0);\n"epepctx_argwire_size;pr" CHECK(\"random buffer validates\", EverParseIsSuccess(r));\n";pr" CHECK(\"random position correct\", r == %d);\n"wire_size;pr" }\n"letemit_schema_test?outdirppfswire_size=letprfmt=Fmt.pfppffmtin(* Read the validator name straight out of EverParse's generated [.h]
-- the one authoritative source. EverParse applies its own naming
rules (different for the top-level Validate function vs. the extern
callbacks); any attempt to re-implement them here has drifted before. *)letep=matchoutdirwith|Somedir->read_validate_name~outdir:dirs|None->file_basesinletlower=String.lowercase_asciis.nameinletuses_ctx=Wire.Everparse.uses_wire_ctxsinletctx_arg=ifuses_ctxthen"(WIRECTX *) &ctx, "else""inpr"\n /* %s (%d bytes) */\n"s.namewire_size;pr" {\n";pr" int pass = 0, fail = 0;\n";pr" uint8_t buf[%d];\n"wire_size;pr" uint64_t r;\n";ifuses_ctxthenpr" %sFields ctx = {0};\n"(c_idents);pr"\n";pr" memset(buf, 0, %d);\n"wire_size;emit_sanity_checkppf~name:s.name~ep~ctx_argwire_size;pr" CHECK(\"zero buffer validates\", EverParseIsSuccess(r));\n";pr" CHECK(\"position advanced to %d\", r == %d);\n"wire_sizewire_size;pr"\n";emit_truncation_checksppf~ep~ctx_argwire_size;pr"\n";emit_random_checksppf~ep~ctx_argwire_size;pr"\n";ifuses_ctxthenpr" (void) ctx;\n";pr" printf(\"%s: %%d passed, %%d failed\\n\", pass, fail);\n"lower;pr" failures += fail;\n";pr" }\n"letgenerate_test~outdirschemas=letoc=open_out(Filename.concatoutdir"test.c")inletppf=Format.formatter_of_out_channelocinletprfmt=Fmt.pfppffmtinpr"#include <stdio.h>\n";pr"#include <stdlib.h>\n";pr"#include <stdint.h>\n";pr"#include <string.h>\n";pr"#include \"EverParse.h\"\n";letfixed_schemas=List.filter_map(funs->Option.map(funws->(s,ws))s.wire_size)schemasinList.iter(fun(s,_)->letbase=file_basesinpr"#include \"%s.h\"\n"base;ifWire.Everparse.uses_wire_ctxsthenpr"#include \"%s_Fields.h\"\n"base)fixed_schemas;(* counting_error_handler is only referenced from the per-schema test
blocks emit_schema_test emits, which run only for fixed-size schemas.
Skip it entirely when there are none, otherwise -Wunused-function
under strict flags rejects the file. *)iffixed_schemas<>[]thenbeginpr"\nstatic int error_count;\n\n";pr"static void counting_error_handler(\n";pr" EVERPARSE_STRING t, EVERPARSE_STRING f, EVERPARSE_STRING r,\n";pr" uint64_t c, uint8_t *ctx, uint8_t *i, uint64_t p) {\n";pr" (void)t; (void)f; (void)r; (void)c; (void)ctx; (void)i; (void)p;\n";pr" error_count++;\n";pr"}\n\n"end;pr"#define CHECK(msg, cond) do { \\\n";pr" if (cond) { pass++; } \\\n";pr" else { fail++; fprintf(stderr, \" FAIL: %%s\\n\", msg); } \\\n";pr"} while(0)\n\n";pr"int main(void) {\n";pr" int failures = 0;\n";List.iter(fun(s,ws)->emit_schema_test~outdirppfsws)fixed_schemas;pr"\n if (failures == 0)\n";pr" printf(\"All tests passed.\\n\");\n";pr" else\n";pr" printf(\"%%d test(s) failed.\\n\", failures);\n";pr" return failures ? 1 : 0;\n";pr"}\n";Format.pp_print_flushppf();close_outocletensure_diroutdir=tryUnix.mkdiroutdir0o755withUnix.Unix_error(Unix.EEXIST,_,_)->()letgenerate_3d~outdirschemas=ensure_diroutdir;write_3d~outdirschemasletrm_rfdir=(trySys.readdirdirwithSys_error_->[||])|>Array.iter(funf->trySys.remove(Filename.concatdirf)withSys_error_->());trySys.rmdirdirwithSys_error_->()letdefault_job_count()=max1(min4(Domain.recommended_domain_count()))(* A bounded fork pool: each job runs in its own process, so blocking EverParse
runs ([Sys.command]) overlap across cores with full isolation. Returns each
job's success (exit 0 and no exception) in input order. EverParse runs must
be isolated because they race on shared intermediate files in a shared
directory, so jobs that invoke [3d.exe] should each use a private cwd. *)letfork_pool~max_jobsjobs=letn=Array.lengthjobsinletok=Array.makenfalseinletpid_idx=Hashtbl.create64inletnext=ref0andrunning=ref0inletreap()=letpid,status=Unix.wait()inmatchHashtbl.find_optpid_idxpidwith|Somei->Hashtbl.removepid_idxpid;decrrunning;ok.(i)<-(matchstatuswithUnix.WEXITED0->true|_->false)|None->()in(* Drain any buffered output before forking, so a child does not inherit and
re-flush the parent's pending bytes. *)Format.pp_print_flushFmt.stderr();Format.pp_print_flushFmt.stdout();while!next<n||!running>0doif!next<n&&!running<max_jobsthenbeginleti=!nextinincrnext;matchUnix.fork()with|0->(tryjobs.(i)();Unix._exit0withe->Fmt.epr"%s\n%!"(Printexc.to_stringe);Unix._exit1)|pid->Hashtbl.addpid_idxpidi;incrrunningendelsereap()done;ok(* Verify every schema through EverParse, the way its own corpus is tested (one
schema per .3d module, each accepted iff F* verifies it). The cost is
dominated by per-module F* verification (CPU-bound, several seconds each);
per-invocation startup is negligible, so a single [3d.exe --batch] over
everything just verifies serially on one core. The schemas are instead
verified concurrently in a {!fork_pool} (at most [max_jobs] at once), each
[3d.exe] run in its own directory (concurrent runs race on EverParse's shared
intermediate files). The pool overlaps the per-module work across cores and
load-balances as runs finish, the only lever for this cost. [Ok ()] iff
EverParse accepts every schema, else [Error] naming the offending schema(s)
with their captured diagnostics. The caller provides schemas with distinct
names (each becomes its own .3d module). *)letbatch_check?max_jobs~outdirschemas=match(locate_3d_exe(),schemas)with|None,_->Error"3d.exe not found in PATH or ~/.local/everparse/bin/"|Some_,[]->Ok()|Someexe,_->(ensure_diroutdir;letarr:tarray=Array.of_listschemasinletlog_ofi=Filename.concatoutdir(arr.(i).name^".batchlog")inletjobs=Array.mapi(funischema()->letwork=Filename.temp_dir"wire_batchchk"""inFun.protect~finally:(fun()->rm_rfwork)(fun()->generate_3d~outdir:work[schema];letcmd=Fmt.str"cd %s && %s --batch --no_copy_everparse_h %s > %s 2>&1"workexe(Wire.Everparse.filenameschema)(Filename.quote(log_ofi))inifSys.commandcmd<>0thenfailwith"EverParse rejected"))arrinletmax_jobs=Option.valuemax_jobs~default:(default_job_count())inletok=fork_pool~max_jobsjobsinleterrors=Array.to_listok|>List.mapi(funipassed->ifpassedthenNoneelseletmsg=tryIn_channel.with_open_text(log_ofi)In_channel.input_allwithSys_error_->""inFmt.kstr(funs->Somes)"%s:\n%s"arr.(i).namemsg)|>List.filter_mapFun.idinmatcherrorswith[]->Ok()|_->Error(String.concat"\n"errors))letgenerate_c?(quiet=true)~outdirschemas=ensure_diroutdir;ifhas_3d_exe()thenbeginrun_everparse~quiet~outdirschemas;write_external_typedefs~outdirschemas;write_fields~outdirschemas;generate_test~outdirschemasendelsefailwith"3d.exe not found in PATH. Install EverParse to regenerate C files."letrun?(quiet=true)~outdirschemas=generate_3d~outdirschemas;generate_c~quiet~outdirschemas(* Strict C11 with warnings-as-errors. [_DEFAULT_SOURCE] declares the BSD endian
helpers (be16toh, ...) the generated C uses from <endian.h> on Linux glibc.
[-Wextra] is deliberately omitted: its [-Wtype-limits] flags the always-true
[0U <= unsigned] bound check EverParse emits for an optional's absent 0-byte
case (an empty case is a 3D syntax error, so the zero-byte field is forced),
which is not a strict-C11 violation. The generated validators compile clean
under this set. *)letstrict_cc_flags="-std=c11 -D_DEFAULT_SOURCE -Wall -Werror -Wpedantic -Wstrict-prototypes \
-Wmissing-prototypes -Wshadow -Wcast-qual"(* EverParse's generated wrapper types a parameterized validator's parameters
with the 3D type name (e.g. [UINT16BE max_len]) but defines none of those
names (the validator itself uses [uint16_t]). Map each 3D integer type to its
host-order C type so the wrapper compiles. Harmless for non-parameterized
wrappers, which never reference them. *)leteverparse_type_defines="-DUINT8=uint8_t -DUINT16=uint16_t -DUINT16BE=uint16_t -DUINT32=uint32_t \
-DUINT32BE=uint32_t -DUINT64=uint64_t -DUINT64BE=uint64_t"letemit_gen_rulesppfthree_d_filesc_filesctx_files=Fmt.pfppf"(rule\n\
\ (alias 3d)\n\
\ (mode promote)\n\
\ (targets %s)\n\
\ (action\n\
\ (run %%{exe:gen.exe} 3d)))\n\n\
(rule\n\
\ (alias 3d)\n\
\ (enabled_if\n\
\ (= %%{env:BUILD_EVERPARSE=} \"1\"))\n\
\ (mode promote)\n\
\ (targets EverParse.h EverParseEndianness.h %s test.c)\n\
\ (deps %s)\n\
\ (action\n\
\ (run %%{exe:gen.exe} c)))\n\n"(String.concat" "three_d_files)(String.concat" "(c_files@ctx_files))(String.concat" "three_d_files)letemit_runtest_ruleppf~test_bin~all_deps~c_srcs=Fmt.pfppf"(rule\n\
\ (targets %s)\n\
\ (deps %s)\n\
\ (action\n\
\ (run cc %s -o %s test.c %s)))\n\n\
(rule\n\
\ (alias runtest)\n\
\ (deps %s)\n\
\ (action\n\
\ (run %%{dep:%s})))\n\n"test_bin(String.concat" "all_deps)strict_cc_flagstest_bin(String.concat" "c_srcs)test_bintest_binletemit_install_stanzappf~package~three_d_files~c_files~ctx_files=letprfmt=Fmt.pfppffmtinpr"(install\n (package %s)\n (section lib)\n (files\n"package;List.iter(funf->pr" (%s as c/%s)\n"ff)three_d_files;List.iter(funf->pr" (%s as c/%s)\n"ff)c_files;List.iter(funf->pr" (%s as c/%s)\n"ff)ctx_files;pr" (EverParse.h as c/EverParse.h)\n";pr" (EverParseEndianness.h as c/EverParseEndianness.h)))\n"letgenerate_dune~outdir~packageschemas=letoc=open_out(Filename.concatoutdir"dune.inc")inletppf=Format.formatter_of_out_channelocinletnames=List.mapfile_baseschemasinletc_files=List.concat_map(funn->[n^".h";n^".c"])namesinletctx_files=wire_ctx_filesschemasinletfields_srcs=fields_c_filesschemasinletthree_d_files=List.map(funn->n^".3d")namesinlettest_bin="test_"^String.map(func->ifc='-'then'_'elsec)packageinletall_deps=["test.c";"EverParse.h";"EverParseEndianness.h"]@c_files@ctx_filesinletc_srcs=List.map(funn->n^".c")names@fields_srcsinemit_gen_rulesppfthree_d_filesc_filesctx_files;emit_runtest_ruleppf~test_bin~all_deps~c_srcs;emit_install_stanzappf~package~three_d_files~c_files~ctx_files;Format.pp_print_flushppf();close_outoc(* A codec awaiting projection. [main] and the standalone helpers take these
rather than an already-projected [Wire.Everparse.t] so the caller never has
to choose the projection mode: it is picked here from [main]'s [~mode], not at
the call site, which removes any [~mode] + projection-mode mismatch. *)typepacked=Pack:'aWire.Codec.t->packedletpackc=Packc(* -- Documentation / single-file pipeline. [main ~mode:`Standalone] drives these:
project each codec with [Wire.Everparse.project] (FFI-free), merge the family
into one [<Package>.3d] with [write], and compile that single spec to a
validator-only [<Package>.c] -- no per-codec files, no [_Fields] plug, no FFI
stubs. The package name becomes the 3D module name, so turn it into a valid
EverParse identifier (CamelCase, no hyphens): "my-pkg" -> "MyPkg". -- *)letdoc_module_namepackage=letalnumc=(c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9')inpackage|>String.map(func->ifalnumcthencelse'_')|>String.split_on_char'_'|>List.filter(funs->s<>"")|>List.mapString.capitalize_ascii|>String.concat""(* The 3D module / file base for the doc pipeline: [?name] when given, else the
opam [~package]. Lets the emitted [<Base>.3d] / [.c] be named independently of
the install package (whose name [~package] keeps for the install stanza). *)letstandalone_base?name~package()=doc_module_name(matchnamewithSomen->n|None->package)(* -- Differential self-check for the doc pipeline. The doc projection emits a
validator-only C parser with no FFI, so nothing otherwise confirms that the
generated validator accepts exactly the inputs the OCaml codec accepts: a
bug in the projection (wrong bit order, a constraint that means something
different over the wire type) would pass unnoticed. [generate_corpus] prints
fuzzed inputs tagged with the OCaml verdict, and [generate_agree] emits a C
program that runs the validator on each and fails on any disagreement. -- *)lethex_of_bytesb=letbuf=Buffer.create(Bytes.lengthb*2)inletppf=Fmt.with_bufferbufinBytes.iter(func->Fmt.pfppf"%02x"(Char.codec))b;Format.pp_print_flushppf();Buffer.contentsbuf(* The accept/reject decision the validator must reproduce: the OCaml codec
decodes (structure plus constraints), validates (constraints and
where-clauses), and the record spans the whole buffer -- the hardened
[<Base>Check<Codec>] wrapper rejects trailing bytes (see [harden_wrapper]),
so the oracle must too. *)letcodec_accepts?envcbuf=matchWire.Codec.decode?envcbuf0with|Ok_->(tryWire.Codec.validate?envcbuf0;Wire.Codec.wire_size_atcbuf0=Bytes.lengthbufwithWire.Parse_error_->false)|Error_->false(* A small param value, biased around the codec's footprint so a parameter that
bounds a field size (e.g. a max length) straddles the accept/reject boundary
rather than always accepting or always rejecting. *)letfuzz_param_valuerngcenter=matchRandom.State.intrng6with|0->0|1->center|2->center+1|3->Random.State.intrng(max1((2*center)+4))|_->Random.State.intrng(max1(center+1))(* Structurally-biased lengths: cluster around the codec's minimum size so the
corpus straddles the accept/reject boundary. The exact size and a little
over exercise the constraint path; under-size exercises truncation; a few
far-out lengths add breadth. *)letfuzz_lengthrngcenter=matchRandom.State.intrng10with|0->0|1|2->Random.State.intrng(max1(center+1))|3->(2*center)+Random.State.intrng4|4->center+1|_->centerletgenerate_corpus?(count=256)ppfcodecs=letrng=Random.State.make[|0x5eed51|]inList.iter(fun(Packc)->lets=project~mode:`Standalonecinletname=s.nameinletpnames=matchs.sourcewithSomest->Raw.input_param_namesst|None->[]inletcenter=Wire.Codec.min_wire_sizecinfor_=1tocountdoletlen=fuzz_lengthrngcenterinletb=Bytes.initlen(fun_->Char.chr(Random.State.intrng256))inletpvals=List.map(fun_->fuzz_param_valuerngcenter)pnamesin(* Seed the validator env with the same param values the C wrapper will
be given, so the two agree on what they validated against. *)letenv=matchpnameswith|[]->None|_->Some(List.fold_left2(funenv->Wire.Param.bind_by_namenve)(Wire.Codec.envc)pnamespvals)inletpfield=matchpvalswith|[]->"-"|_->String.concat","(List.mapstring_of_intpvals)inlethex=iflen=0then"-"elsehex_of_bytesbinFmt.pfppf"%s %s %s %d@\n"namepfieldhex(ifcodec_accepts?envcbthen1else0)done)codecs;Format.pp_print_flushppf()letemit_agree_preambleppfbase~has_params=letprfmt=Fmt.pfppf(fmt^^"@\n")inpr"/* Differential check: the EverParse validator must accept exactly the";pr" inputs the OCaml codec accepts. Reads `<codec> <params> <hex>";pr" <verdict>` lines from gen.exe's corpus, passing each codec's";pr" parameters to its validator, and exits nonzero on any disagreement. */";pr"#include <stdio.h>";pr"#include <stdlib.h>";pr"#include <string.h>";pr"#include <stdint.h>";pr"#include \"%s.h\""base;pr"#include \"%sWrapper.h\""base;pr"";(* The wrapper declares this error sink only in its .c, so declare it here too
to satisfy -Wmissing-prototypes before defining it as a no-op. *)pr"void %sEverParseError(const char *s, const char *f, const char *r);"base;pr"void %sEverParseError(const char *s, const char *f, const char *r)"base;pr"{ (void) s; (void) f; (void) r; }";(* Only emit the parameter parser when a codec actually has parameters: a
package of plain (non-parameterized) codecs never calls it, and an unused
static function is a -Werror under the strict flags. *)ifhas_paramsthenbeginpr"";pr"/* Parse the corpus's comma-separated parameter values. */";pr"static void parse_params(const char *s, unsigned long *out, int n) {";pr" const char *p = s;";pr" for (int i = 0; i < n; i++) {";pr" out[i] = strtoul(p, NULL, 10);";pr" const char *c = strchr(p, ',');";pr" if (c == NULL) break;";pr" p = c + 1;";pr" }";pr"}"endletemit_agree_runppftriples=letprfmt=Fmt.pfppf(fmt^^"@\n")inpr"";pr"static int run(const char *name, const char *params, uint8_t *base, \
uint32_t len) {";pr" (void) params;";List.iter(fun(cname,check,ptypes)->letn=List.lengthptypesinifn=0thenpr" if (strcmp(name, \"%s\") == 0) return %s(base, len) ? 1 : 0;"cnamecheckelsebeginletargs=ptypes|>List.mapi(funit->Fmt.str"(%s) p[%d]"ti)|>String.concat", "inpr" if (strcmp(name, \"%s\") == 0) {"cname;pr" unsigned long p[%d];"n;pr" parse_params(params, p, %d);"n;pr" return %s(%s, base, len) ? 1 : 0;"checkargs;pr" }"end)triples;pr" fprintf(stderr, \"agree: unknown codec '%%s'\\n\", name);";pr" exit(3);";pr"}"letemit_agree_mainppf=letprfmt=Fmt.pfppf(fmt^^"@\n")inpr"";pr"int main(int argc, char **argv) {";pr" if (argc < 2) { fprintf(stderr, \"usage: %%s <corpus>\\n\", argv[0]); \
return 2; }";pr" FILE *fp = fopen(argv[1], \"r\");";pr" if (!fp) { perror(\"fopen\"); return 2; }";pr" char name[256];";pr" char params[4096];";(* [hex] needs two chars per [buf] byte plus the NUL; one char short truncates
the corpus line and misparses the verdict as a false mismatch. *)pr" uint8_t buf[65536];";pr" char hex[2 * sizeof(buf) + 1];";pr" long verdict, total = 0, mismatch = 0;";pr" while (fscanf(fp, \"%%255s %%4095s %%131072s %%ld\", name, params, hex, \
&verdict) == 4) {";pr" uint32_t len = 0;";pr" if (strcmp(hex, \"-\") != 0) {";pr" size_t hl = strlen(hex);";pr" len = (uint32_t) (hl / 2);";pr" if (len > sizeof(buf)) { fprintf(stderr, \"input too long\\n\"); \
fclose(fp); return 2; }";pr" for (uint32_t i = 0; i < len; i++) {";pr" unsigned b;";pr" if (sscanf(hex + 2 * i, \"%%2x\", &b) != 1) { fprintf(stderr, \
\"bad hex\\n\"); fclose(fp); return 2; }";pr" buf[i] = (uint8_t) b;";pr" }";pr" }";pr" int accept = run(name, params, buf, len);";pr" total++;";pr" if (accept != (int) verdict) {";pr" mismatch++;";pr" if (mismatch <= 20)";pr" fprintf(stderr, \"MISMATCH codec=%%s len=%%u validator=%%d \
oracle=%%ld\\n\", name, len, accept, verdict);";pr" }";pr" }";pr" fclose(fp);";pr" fprintf(stdout, \"agree: %%ld inputs, %%ld mismatches\\n\", total, \
mismatch);";pr" return mismatch == 0 ? 0 : 1;";pr"}"(* EverParse names each entrypoint wrapper [<Base>Check<Codec>] and gives it the
input parameters before the trailing [base, len], so both the helper name and
its parameter C types follow from the codec alone. Deriving them here keeps
[agree.c] pure OCaml (no reading of the generated [<Base>Wrapper.h]); a wrong
name would surface as a link error when the differential test compiles
[agree.c] against the real wrapper. *)letgenerate_agree?name~outdir~packagecodecs=letbase=standalone_base?name~package()inlettriples=List.map(fun(Packc)->lets=project~mode:`Standalonecinletptypes=matchs.sourcewith|Somest->Raw.input_param_c_typesst|None->[]in(* EverParse builds the wrapper symbol as
[pascal_case (module ^ "_check_" ^ codec)], so compute it the same way
rather than gluing pre-normalized parts: only the whole-string
[pascal_case] gets the casing right for names like [TPM2B -> Tpm2b]. *)(s.name,pascal_case(base^"_check_"^s.name),ptypes))codecsinlethas_params=List.exists(fun(_,_,ptypes)->ptypes<>[])triplesinletoc=open_out(Filename.concatoutdir"agree.c")inletppf=Format.formatter_of_out_channelocinemit_agree_preambleppfbase~has_params;emit_agree_runppftriples;emit_agree_mainppf;Format.pp_print_flushppf();close_outocletgenerate_3d_standalone?name~outdir~packagecodecs=ensure_diroutdir;write~mode:`Standalone~outdir~name:(standalone_base?name~package())(List.map(fun(Packc)->project~mode:`Standalonec)codecs)letgenerate_c_standalone?(quiet=true)?name~outdir~package()=ensure_diroutdir;ifhas_3d_exe()thenrun_everparse_files~quiet~outdir[standalone_base?name~package()^".3d"]elsefailwith"3d.exe not found in PATH. Install EverParse to regenerate C files."letgenerate_standalone?(quiet=true)?name~outdir~packagecodecs=generate_3d_standalone?name~outdir~packagecodecs;generate_c_standalone~quiet?name~outdir~package();generate_agree?name~outdir~packagecodecs(* The standalone [c/] archive is a per-target artefact: a consumer links it
into a program for the target it was built for, so a cross build must produce
a target archive and install it into the cross toolchain, exactly as it does
the host archive for the host toolchain. [emit_standalone_build_rules] gets
this by building through the context's own toolchain -- [%{cc}], the
[ocaml-config] partial linker, and the binutils that compiler resolves -- so
the tools follow the build context rather than a fixed host.
Two things stay host-only, gated on [%{context_name}] (a cross context is
named [<host-context>.<target>], so the host is the one still called
[default]): regenerating the committed [.3d]/C from [3d.exe] (a host
developer action; the committed C is what a cross build compiles), and the
[agree] differential test (it runs the built validator, which a cross build
produces for the target, not the host). *)lethost_context="(= %{context_name} default)"(* [(and)] of the host-context gate and [cond], laid out as [dune fmt] prints
it (the one-line form runs past the margin). *)lethost_context_andcond=Fmt.str"(and\n %s\n %s)"host_contextcond(* The [.3d] and [agree.c] are pure OCaml ([gen.exe 3d] / [gen.exe agree]), so
they regenerate on demand and never go stale. The C needs [3d.exe], so its
rule exists only under [BUILD_EVERPARSE=1] ([mode promote] writes it back into
the tree); a plain [dune build] uses the committed C and never invokes
[3d.exe], and fails loudly if the C was never committed rather than silently
shelling out. A codec change refreshes the [.3d] and [agree.c] while the
committed C goes stale -- the runtest differential below catches that, since
it regenerates the corpus and [agree.c] from the current codec and runs them
against the committed validator. *)letemit_standalone_gen_rulesppf~three_d~c_files=Fmt.pfppf"(rule\n\
\ (alias 3d)\n\
\ (enabled_if\n\
\ %s)\n\
\ (mode promote)\n\
\ (targets %s)\n\
\ (action\n\
\ (run %%{exe:gen.exe} 3d)))\n\n\
(rule\n\
\ (enabled_if\n\
\ %s)\n\
\ (targets agree.c)\n\
\ (action\n\
\ (run %%{exe:gen.exe} agree)))\n\n\
(rule\n\
\ (alias 3d)\n\
\ (enabled_if\n\
\ %s)\n\
\ (mode promote)\n\
\ (targets EverParse.h EverParseEndianness.h %s)\n\
\ (deps %s)\n\
\ (action\n\
\ (run %%{exe:gen.exe} c)))\n\n"host_contextthree_dhost_context(host_context_and"(= %{env:BUILD_EVERPARSE=} \"1\")")(String.concat" "c_files)three_d(* The C symbols a standalone archive exports: the [<Base>Check<Codec>] wrapper
for each codec, named exactly as EverParse names it (and as [agree.c] links
it, see [generate_agree]), so the export allowlist matches the real symbol. *)letwrapper_symbolsbasecodecs=List.map(fun(Packc)->pascal_case(base^"_check_"^(project~mode:`Standalonec).name))codecs(* Post-compile steps that fold the compiled objects into one archive member
exporting only [wrappers], localizing the raw [<Base>Validate*] entry points
so their unguarded [StartPosition] (see [emit_standalone_install]) is not
reachable through the installed archive. macOS localizes during the partial
link ([ld -r -exported_symbol]); GNU [ld] cannot, so [objcopy] does it after.
Shared by the emitted dune rule and the symbol-hiding test so they cannot
drift. *)letarchive_link_steps~macos~pack_linker~objcopy~ar~archive~base~wrappers=letlibo=base^"_lib.o"inletobjs=Fmt.str"%s.o %sWrapper.o"basebaseinifmacosthen[Fmt.str"%s %s %s%s"pack_linkerliboobjs(List.fold_left(funaw->a^" -exported_symbol _"^w)""wrappers);Fmt.str"%s rcs %s %s"ararchivelibo;]else[Fmt.str"%s %s %s"pack_linkerliboobjs;Fmt.str"%s%s %s"objcopy(List.fold_left(funaw->a^" --keep-global-symbol "^w)""wrappers)libo;Fmt.str"%s rcs %s %s"ararchivelibo;](* Differential self-check: the OCaml codec's accept/reject verdict over a
fuzzed corpus must match the committed C validator's, or the doc projection
is unsound (or the committed C is stale). The corpus and the [agree] driver
are built as targets and run directly, so no shell is involved: the generator
is referenced through [%{exe:gen.exe}], which records the dependency and
resolves the sandbox path (a bare [./gen.exe] is not reliably present in the
action's cwd). Uses only gen.exe and cc, no 3d.exe. *)letemit_standalone_check_rulesppf~base~archive=Fmt.pfppf"(rule\n\
\ (enabled_if\n\
\ %s)\n\
\ (targets corpus)\n\
\ (action\n\
\ (with-stdout-to corpus (run %%{exe:gen.exe} corpus))))\n\n\
(rule\n\
\ (enabled_if\n\
\ %s)\n\
\ (targets agree)\n\
\ (deps agree.c %s EverParse.h EverParseEndianness.h %s.h %sWrapper.h)\n\
\ (action\n\
\ (run cc %s %s agree.c %s -o agree)))\n\n\
(rule\n\
\ (alias runtest)\n\
\ (enabled_if\n\
\ %s)\n\
\ (deps corpus agree)\n\
\ (action\n\
\ (run %%{dep:agree} corpus)))\n\n"host_contexthost_contextarchivebasebasestrict_cc_flagseverparse_type_definesarchivehost_context(* Build the validator into an archive, installed with the package, so consumers
get a ready-to-link library and a downstream build fails loudly if the spec
stops projecting to compilable C. The archive exports only the checked
[<Base>Check<Codec>] wrappers and localizes the raw validators.
The build runs in every context through that context's own toolchain, so a
cross build produces a target archive: [CC] is the context C compiler
([ocaml-config:c_compiler]), the partial link is the [ocaml-config] pack
linker, and the symbol-hiding [objcopy]/[ar] are the ones that compiler
resolves ([-print-prog-name]) -- a cross [cc] finds the target's binutils. A
shell runs the whole thing so that command substitution can resolve the
binutils; there is no dune variable for [objcopy] or [ar]. The localizing step
is still platform-specific ([macos] localizes during the partial link,
elsewhere [objcopy] does it after), keyed on [ocaml-config:system]: for the
host context it names the host, for a cross context the target, which is what
selects the right localize path either way. *)letemit_standalone_build_rulesppf~base~archive~c_files~wrappers=letcompilecc=Fmt.str"%s %s %s -c %s.c %sWrapper.c"ccstrict_cc_flagseverparse_type_definesbasebaseinletemit_rule~cond~macos=letsteps=compile"\"$CC\""::archive_link_steps~macos~pack_linker:"%{ocaml-config:native_pack_linker}"~objcopy:"\"$(\"$CC\" -print-prog-name=objcopy)\""~ar:"\"$(\"$CC\" -print-prog-name=ar)\""~archive~base~wrappersinletscript="set -e; CC=%{ocaml-config:c_compiler}; "^String.concat"; "stepsin(* The script is one dune-quoted string argument to [sh -c]; its own double
quotes (around [$CC] and the [$(...)] tool lookups) are escaped so they
do not close the dune string. *)letquoted=String.concat"\\\""(String.split_on_char'"'script)inFmt.pfppf"(rule\n\
\ (targets %s)\n\
\ (enabled_if\n\
\ %s)\n\
\ (deps EverParse.h EverParseEndianness.h %s)\n\
\ (action\n\
\ (run sh -c \"%s\")))\n\n"archivecond(String.concat" "c_files)quotedinemit_rule~cond:"(= %{ocaml-config:system} macosx)"~macos:true;emit_rule~cond:"(<> %{ocaml-config:system} macosx)"~macos:false;emit_standalone_check_rulesppf~base~archive(* Install only the checked wrapper header, not the raw validator header. The
[<Base>Validate*] entrypoints in [<Base>.h] take a [StartPosition] and their
EverParse-emitted preamble bounds a read as [N <= InputLength - StartPosition]
with no prior [StartPosition <= InputLength] check, so a direct C caller
passing [StartPosition > InputLength] underflows the span (unsigned) and reads
out of bounds. The generated wrapper [<Base>Check<Codec>(base, len)] always
validates from position 0 and is the safe public C API; the raw validators
stay build-internal, linked into the archive but not shipped as a header.
[<Base>Wrapper.h] does not include [<Base>.h], so this compiles standalone. *)letemit_standalone_installppf~package~three_d~archive~public_header=letprfmt=Fmt.pfppffmtinpr"(install\n (package %s)\n (section lib)\n (files\n"package;List.iter(funf->pr" (%s as c/%s)\n"ff)[three_d;archive;public_header];pr" (EverParse.h as c/EverParse.h)\n";pr" (EverParseEndianness.h as c/EverParseEndianness.h)))\n"letgenerate_dune_standalone?name~outdir~packagecodecs=letbase=standalone_base?name~package()inletthree_d=base^".3d"inletc_files=[base^".c";base^".h";base^"Wrapper.c";base^"Wrapper.h"]inletarchive="lib"^String.lowercase_asciibase^".a"inletwrappers=wrapper_symbolsbasecodecsinletoc=open_out(Filename.concatoutdir"dune.inc")inletppf=Format.formatter_of_out_channelocinemit_standalone_gen_rulesppf~three_d~c_files;emit_standalone_build_rulesppf~base~archive~c_files~wrappers;emit_standalone_installppf~package~three_d~archive~public_header:(base^"Wrapper.h");Format.pp_print_flushppf();close_outocletmain?name~mode~packagecodecs=letargv=Array.to_listSys.argvinmatchmodewith|`Ffi->(letschemas=List.map(fun(Packc)->project~mode:`Ffic)codecsinmatchargvwith|[_;"3d"]->generate_3d~outdir:"."schemas|[_;"c"]->generate_c~outdir:"."schemas|[_;"dune"]->generate_dune~outdir:"."~packageschemas|_->run~outdir:"."schemas)|`Standalone->(matchargvwith|[_;"3d"]->generate_3d_standalone?name~outdir:"."~packagecodecs|[_;"c"]->generate_c_standalone?name~outdir:"."~package()|[_;"agree"]->generate_agree?name~outdir:"."~packagecodecs|[_;"dune"]->generate_dune_standalone?name~outdir:"."~packagecodecs|[_;"corpus"]->generate_corpusFormat.std_formattercodecs|_->generate_standalone?name~outdir:"."~packagecodecs)