diff --git a/Bin/CalC.c b/Bin/CalC.c new file mode 100644 index 0000000..b10e794 --- /dev/null +++ b/Bin/CalC.c @@ -0,0 +1,440 @@ +#include +#include + +/// +/// CalC -- a small immediate-mode calculator, the running playground for the +/// parser-combinator DSL. It parses and evaluates as it reads (no AST): every +/// rule folds its result straight into a `Num`. Variables live in the grammar +/// context, and errors are collected greedily: a broken sub-expression records a +/// `PcReport`, poisons its value, and parsing continues, so one line reports as +/// many problems as it has. +/// +/// line = assignment | expr +/// assignment = identifier '=' expr (stores into ctx->vars) +/// expr = additive +/// additive = multiplicative ( ('+'|'-') multiplicative )* left-assoc +/// multiplicative = unary ( ('*'|'/'|'%') unary )* left-assoc +/// unary = ('-'|'+')? atom +/// atom = '(' expr ')' | identifier | number +/// number = digit+ ( '.' digit+ )? (int, or float if a '.') +/// identifier = letter+ (a variable name) +/// + +typedef struct Num { + bool is_float; + bool is_error; ///< poison: a broken sub-expression; propagates, never aborts + union { + i64 i; + f64 f; + }; +} Num; + +static Num num_int(i64 v) { + return (Num) {.i = v}; +} +static Num num_flt(f64 v) { + return (Num) {.is_float = true, .f = v}; +} +static Num num_poison(void) { + return (Num) {.is_error = true}; +} +static f64 num_f(Num n) { + return n.is_float ? n.f : (f64)n.i; +} +static bool num_is_zero(Num n) { + return n.is_float ? (n.f == 0.0) : (n.i == 0); +} + +static Num num_add(Num a, Num b) { + if (a.is_error || b.is_error) + return num_poison(); + return (a.is_float || b.is_float) ? num_flt(num_f(a) + num_f(b)) : num_int(a.i + b.i); +} +static Num num_sub(Num a, Num b) { + if (a.is_error || b.is_error) + return num_poison(); + return (a.is_float || b.is_float) ? num_flt(num_f(a) - num_f(b)) : num_int(a.i - b.i); +} +static Num num_mul(Num a, Num b) { + if (a.is_error || b.is_error) + return num_poison(); + return (a.is_float || b.is_float) ? num_flt(num_f(a) * num_f(b)) : num_int(a.i * b.i); +} +static Num num_div(Num a, Num b) { + if (a.is_error || b.is_error || num_is_zero(b)) + return num_poison(); + return (a.is_float || b.is_float) ? num_flt(num_f(a) / num_f(b)) : num_int(a.i / b.i); +} +static Num num_mod(Num a, Num b) { + if (a.is_error || b.is_error || num_is_zero(b) || a.is_float || b.is_float) + return num_poison(); + return num_int(a.i % b.i); +} +static Num num_neg(Num a) { + if (a.is_error) + return num_poison(); + return a.is_float ? num_flt(-a.f) : num_int(-a.i); +} + +typedef struct Binding { + Str name; + Num value; +} Binding; +typedef Vec(Binding) Bindings; + +static void binding_deinit(void *copy, const Allocator *alloc) { + (void)alloc; + StrDeinit(&((Binding *)copy)->name); +} +/// +/// The grammar context threaded through every parser as `ctx`: the variable +/// environment plus the diagnostics sink the `PcReport*` macros append to. Both +/// are containers that carry their own allocator. +/// +typedef struct PcParserCtx { + Bindings vars; + PcReports reports; +} PcParserCtx; + +typedef struct { + u64 len; +} PcParserCtxMark; + +static PcParserCtxMark PcParserCtxSnapshot(PcParserCtx *ctx) { + return (PcParserCtxMark) {.len = VecLen(&ctx->vars)}; +} + +static void PcParserCtxRollback(PcParserCtx *ctx, PcParserCtxMark mark) { + VecResize(&ctx->vars, mark.len); +} + +PcRecognizer(WsChar); +PcRecognizer(Ws); +PcParser(DigitCh, char); +PcParser(LetterCh, char); +PcParser(CharIfExist, char, char); +PcParser(Sym, char, char); +PcParser(SignCh, char); +PcParser(Number, Num); +PcParser(Identifier, Str); +PcParser(VarRef, Num); +PcParser(AddOp, char); +PcParser(MulOp, char); +PcParser(Parenthesized, Num); +PcParser(Atom, Num); +PcParser(Unary, Num); +PcParser(Multiplicative, Num); +PcParser(Additive, Num); +PcParser(Expr, Num); +PcParser(Assignment, Num); +PcParser(Line, Num); +PcParser(Calc, Num); + +PcRecognizer(WsChar) { + PcSatisfyChar(c, c == ' ' || c == '\t' || c == '\n' || c == '\r') {} +} + +PcRecognizer(Ws) { + PcRecognizeZeroOrMore(WsChar); +} + +PcParser(DigitCh, char) { + PcSatisfyChar(c, c >= '0' && c <= '9') { + *value = c; + } +} + +PcParser(LetterCh, char) { + PcSatisfyChar(c, (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_') { + *value = c; + } +} + +/// fundamental: match a specific char (the `expect` input), capture it +PcParser(CharIfExist, char, char) { + PcSatisfyChar(c, c == expect) { + *value = c; + } +} + +/// a token char: match `expect`, then eat trailing whitespace +PcParser(Sym, char, char) { + PcSeq() { + PcMatch(CharIfExist, expect, value); + PcRecognize(Ws); + } +} + +PcParser(SignCh, char) { + PcChoice() { + PcAlt(Sym, '-', value); + PcAlt(Sym, '+', value); + } +} + +/// number = digit+ ( '.' digit+ )? -- accumulated into a Str, converted by the +/// library parser; a value that does not fit is reported and poisoned, not aborted +PcParser(Number, Num) { + char d; + bool is_float = false; + PcSeq() { + StrInitStack(tok, 64) { + PcMatchOneOrMore(DigitCh, &d) { + StrPushBack(&tok, d); + } + PcOpt(CharIfExist, '.', &d) { + is_float = true; + StrPushBack(&tok, d); + PcMatchZeroOrMore(DigitCh, &d) { + StrPushBack(&tok, d); + } + } + if (is_float) { + f64 f = 0; + if (StrToF64(&tok, &f, NULL)) + *value = num_flt(f); + else { + PcReportError("malformed number"); + *value = num_poison(); + } + } else { + u64 u = 0; + if (StrToU64(&tok, &u, NULL)) + *value = num_int((i64)u); + else { + PcReportError("number does not fit in an integer"); + *value = num_poison(); + } + } + } + PcRecognize(Ws); + } +} + +/// identifier = letter+ ; the matched letters are appended into the caller's Str +PcParser(Identifier, Str) { + char l; + PcSeq() { + PcMatchOneOrMore(LetterCh, &l) { + StrPushBack(value, l); + } + PcRecognize(Ws); + } +} + +/// variable reference: look the name up; undefined -> report and poison (keep going) +PcParser(VarRef, Num) { + PcSeq() { + StrInitStack(name, 64) { + PcMatch(Identifier, &name); + Num *slot = NULL; + VecForeachPtrReverse(&ctx->vars, b) if (StrCmp(&b->name, &name) == 0) { + slot = &b->value; + break; + } + if (slot) + *value = *slot; + else { + PcReportError("undefined variable"); + *value = num_poison(); + } + } + } +} + +PcParser(AddOp, char) { + PcChoice() { + PcAlt(Sym, '+', value); + PcAlt(Sym, '-', value); + } +} + +PcParser(MulOp, char) { + PcChoice() { + PcAlt(Sym, '*', value); + PcAlt(Sym, '/', value); + PcAlt(Sym, '%', value); + } +} + +PcParser(Parenthesized, Num) { + char paren; + PcSeq() { + PcMatch(Sym, '(', &paren); + PcMatch(Expr, value); + PcMatch(Sym, ')', &paren); + } +} + +PcParser(Atom, Num) { + PcChoice() { + PcAlt(Parenthesized, value); + PcAlt(VarRef, value); + PcAlt(Number, value); + PcElse() { + PcReportErrorHere("expected an operand"); + PcRecover(c, c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c == ')'); + *value = num_poison(); + } + } +} + +PcParser(Unary, Num) { + char sign = '+'; + PcSeq() { + PcOpt(SignCh, &sign) {} + PcMatch(Atom, value); + if (sign == '-') + *value = num_neg(*value); + } +} + +PcParser(Multiplicative, Num) { + char op; + Num rhs; + PcSeq() { + PcMatch(Unary, value); + PcMatchZeroOrMore(MulOp, &op) { + PcMatch(Unary, &rhs); + bool fresh = !value->is_error && !rhs.is_error; + if (fresh && (op == '/' || op == '%') && num_is_zero(rhs)) { + PcReportError("division by zero"); + *value = num_poison(); + } else if (fresh && op == '%' && (value->is_float || rhs.is_float)) { + PcReportError("modulo needs integer operands"); + *value = num_poison(); + } else + *value = (op == '*') ? num_mul(*value, rhs) : (op == '/') ? num_div(*value, rhs) : num_mod(*value, rhs); + } + } +} + +PcParser(Additive, Num) { + char op; + Num rhs; + PcSeq() { + PcMatch(Multiplicative, value); + PcMatchZeroOrMore(AddOp, &op) { + PcMatch(Multiplicative, &rhs); + *value = (op == '+') ? num_add(*value, rhs) : num_sub(*value, rhs); + } + } +} + +PcParser(Expr, Num) { + return PcParse(Additive, value); +} + +/// assignment = identifier '=' expr ; evaluate, then bind the name (the map copies it) +PcParser(Assignment, Num) { + char eq; + PcSeq() { + StrInitStack(name, 64) { + PcMatch(Identifier, &name); + PcMatch(Sym, '=', &eq); + PcMatch(Expr, value); + if (!value->is_error) { + Binding b = {.name = StrInitFromStr(&name, VecAllocator(&ctx->vars)), .value = *value}; + VecPushBack(&ctx->vars, b); + } + } + } +} + +PcParser(Line, Num) { + PcChoice() { + PcTryAlt(Assignment, value); + PcAlt(Expr, value); + } +} + +PcParser(Calc, Num) { + PcSeq() { + PcRecognize(Ws); + PcMatch(Line, value); + } +} + +/// bind a predefined variable; the map deep-copies the key, so the temporary is freed +static void seed(Bindings *vars, HeapAllocator *heap, Zstr name, Num v) { + Binding b = {.name = StrInitFromCstr(name, ZstrLen(name), heap), .value = v}; + VecPushBack(vars, b); +} + +static void eval_line(PcParserCtx *ctx, Str *line) { + VecClear(&ctx->reports); + StrIter in = StrIterFromStr(*line); + Num out = {0}; + PcParserStatus st = PcRun(Calc, &in, &out); + if (VecLen(&ctx->reports) > 0) { + StrInitStack(diag, 256) { + PcReportsRender(&diag, line, &ctx->reports); + WriteFmt("{}", diag); + } + } else if ((st & PC_PARSER_STATUS_SUCCESS) && !StrIterRemainingLength(&in)) { + if (out.is_float) + WriteFmtLn("{}", out.f); + else + WriteFmtLn("{}", out.i); + } else + WriteFmtLn("error at column {}", StrIterIndex(&in) + 1); +} + +int main(int argc, char **argv) { + HeapAllocator heap = HeapAllocatorInit(); + + PcParserCtx ctx = { + .vars = VecInitWithDeepCopy(NULL, binding_deinit, &heap), + .reports = VecInit(&heap), + }; + seed(&ctx.vars, &heap, "pi", num_flt(3.14159265358979323846)); + seed(&ctx.vars, &heap, "e", num_flt(2.71828182845904523536)); + + ArgParse args = ArgParseInit("calc", "an immediate-mode parser-combinator calculator", &heap); + Zstr expr = NULL; + ArgOptional(&args, "-c", "--command", &expr, "evaluate one expression and exit"); + ArgRun rc = ArgParseRun(&args, argc, argv); + + int status = 0; + if (rc == ARG_RUN_ERROR) { + status = 1; + } else if (rc != ARG_RUN_HELP) { + if (expr != NULL) { + Str cmd = StrInitFromCstr(expr, ZstrLen(expr), &heap); + eval_line(&ctx, &cmd); + StrDeinit(&cmd); + } else { + File fin = FileStdin(); + StrInitStack(line, 4096) { + for (;;) { + WriteFmt("> "); + StrClear(&line); + bool eof = false; + for (;;) { + char c; + i64 n = FileRead(&fin, &c, 1); + if (n <= 0) { + eof = true; + break; + } + if (c == '\n') + break; + StrPushBack(&line, c); + } + if (StrLen(&line) > 0) + eval_line(&ctx, &line); + if (eof) { + WriteFmtLn(""); + break; + } + } + } + } + } + + ArgParseDeinit(&args); + VecDeinit(&ctx.reports); + VecDeinit(&ctx.vars); + HeapAllocatorDeinit(&heap); + return status; +} diff --git a/Conventions/CODING-CONVENTIONS.md b/Conventions/CODING-CONVENTIONS.md index 1056244..88898e6 100644 --- a/Conventions/CODING-CONVENTIONS.md +++ b/Conventions/CODING-CONVENTIONS.md @@ -188,13 +188,8 @@ of the codebase to see them in action. shouldn't be — adjusting state has invariants the mutators enforce. Intentional-corruption tests that need to bypass an invariant (to verify that a validator catches it) write the field directly, with - an inline comment opening with the canonical phrase - `// intentional bypass:` followed by *why* no public accessor / - mutator covers the case. The phrase is grep-able; a stray field - write without it is a review finding. See - `Tests/Std/Allocator.Heap.c`, `Tests/Std/BitVec.Convert.c`, - `Tests/Std/Graph.Init.c`, and the other `Tests/Std/*` Deadend - fixtures for the established shape. + an inline comment explaining why no public accessor or mutator + covers the case. - **Container key types ship `*_hash` and `*_compare`.** Any type meant to be a `Map` key (or `Vec`/`List` element with comparison semantics) must expose two snake_case helpers in the @@ -675,10 +670,7 @@ up-cast. exercise the validators by violating an invariant — bypassing capacity bookkeeping, scrambling a magic value, overrunning a buffer. These tests write fields directly because that's the whole point of - the test; open each such site with the canonical - `// intentional bypass:` comment (same phrase used by the - non-Deadend tests in *Accessor macros are read-only* above) so it - stays grep-able and doesn't get swept on the next pass. + the test. - **Fixture-local owned storage** (e.g. `Vec.Complex.c`'s `char *name` / `int *values` inside a `ComplexItem` that exercises `VecInitWithDeepCopy` callbacks) is fine as raw pointers because the diff --git a/Include/Misra/ParserCombinator.h b/Include/Misra/ParserCombinator.h new file mode 100644 index 0000000..d261477 --- /dev/null +++ b/Include/Misra/ParserCombinator.h @@ -0,0 +1,747 @@ +/// file : misra/parsercombinator.h +/// author : Siddharth Mishra (admin@brightprogrammer.in) +/// This is free and unencumbered software released into the public domain. +/// +/// Parser combinator provides arbitrary parser creation capabilities. +/// +/// Definition: +/// A parser combinator is a high order function over parsers that takes +/// a sequence (in the sense that ordering may matter) of parsers and returns +/// a new parser that is derived from the provided parsers. +/// +/// Parser: +/// type Parser a = String -> Result (a, String) +/// A parser takes a string and returns a value of type a, and the remaining string +/// left to parse. +/// +/// Algebraic interfaces: +/// - Functor: Take the result of a parser and transform it without changing the parser +/// itself. Like parsing a digit sequence and converting it to Int or Float. +/// - Applicative: Run parsers in sequence where later parsers dont depend on +/// earlier parsers. [Context-Free nature] +/// - Monad: Run parsers in sequence where later parser depends on the earlier +/// parser's value. [Context-Sensitive nature] +/// - Alternative: Choice and repetition. +/// + +/// Last year I was tasked to write a C++ demangler in pure C and was getting paid to do it. +/// I knew this person, and was like a mentor to me, I failed them miserably. The parser I wrote +/// was PEG (as I learn now), which was basically a top-down recursive-descent left-to-right parser that I built using +/// very basic macro-defined parser combinators. I really respect this person, and I lost +/// their trust and respect, which hurts whenever my internal dialogue reminds me of it. +/// If I get this right this time, this part of my code is my homage to them. +/// +/// There were macros to define parser rules and then to use the rules. There were macros +/// to help you parse in a sequence or alternation and consume the input along the way. +/// +/// The parser was in the end designed in such a way that it would parse the input as it +/// is consumed, zero tokenization. It was able to automatically backtrack because of it's +/// design, and I was very proud of it. A few things came biting me in the ass very badly +/// later down the line : +/// - [Heavy macro usage], in the sense that macro was expanding to multiple lines +/// and was not dispatching code to handler functions. The issue with this is +/// that debugging is a real PITA, because debuggers dont expand macros. +/// Essentially, single-stepping code in GDB to watch the parser run was hell! +/// - [Left-recursive] nature of the C++ name mangling grammar itself. Left recursive +/// grammar is a real hell if you are writing a recursive descent parser and you +/// let this nature of grammar slip away from your eyes. There are multiple normalized +/// forms that can help you but it's still a pain, for very large grammar (that C++ name +/// mangling grammar already is) is a real pain. +/// - [Backtracking] parsers have issue with `Alternative` combinators. They try the next +/// in alternation when the first one fails. Libraries like Parsec in Haskell split this +/// into two behaviors : +/// - If parsing the very first in an alternation fails and it failed by not cosuming a single +/// token, then do not parse anything in the alternative sequence at all. +/// - You can force the alternation parsing anyhow by a `try` keyword that forces the combinator +/// to continue trying alternatives. +/// I believe this would've solved a big chunk of my issues. +/// - [Ordering of alternatives] mattered in my parser. Sometimes I wanted the parser to take +/// a longer parsing route, but the way I wrote the grammar using combinators made the parser +/// take the first smallest route it was able to parse. This created a lot of debugging time +/// which was already hard. +/// - Normalized grammars are hard to make any sense of! If possible, normalize parts of the grammars. +/// Depending on how long each rule is, the number of rules blow up and stop making any sense. +/// +/// Now, I'm standing on the same cliff, and this time I want to make a different decision. +/// +/// I'm using an LLM to learn how other languages and libraries have achieved the same thing. +/// I know this is possible, and I just have to make some tweaks in the way I designed my +/// combinators the last time. The approach this time is to try to imitate how functional +/// languages achieve this same thing. The functional part can be emulated using macros +/// as code-generators hopefully. +/// +/// On the [Heavy macro usage] point, I know that I use lots of macros in this library. +/// That is mainly because I know the real reason debuggability vanishes is not because +/// it's a macro, but because I used macros in a way that it hid the implementation. +/// In this library I made sure that the macros are very thin wrappers and are essentially +/// code-dispatchers first and then code-generators. This ensures that most of the buggy +/// remains debuggable through a function that actually handles the implementation. +/// + +#ifndef MISRA_PARSER_COMBINATOR_H +#define MISRA_PARSER_COMBINATOR_H + +#include +#include +#include +#include +#include + +/// +/// The cursor type. Text grammars use the default `StrIter` (`Iter(char)`); a byte-oriented +/// grammar sets `#define PC_ITER BufIter` (`Iter(const u8)`) before including this header. The +/// block frames are cursor-agnostic (they only use `in->pos` / `*in` / `IterIndex`); only the +/// atoms are element-specific -- `PcSatisfy*` for characters, the `PcU*` family below for bytes. +/// +#ifndef PC_ITER +# define PC_ITER StrIter +#endif + +/// +/// A parser combinator does not only need a way to parse a string. It also needs to convert +/// it to a representation that any phase after parsing can use and analyze. This may heavily +/// depend on the language being parsed. +/// +/// The mechanism this library uses is a per-grammar context (`PcParserCtx`, see below) threaded +/// through every parser. The grammar author places their in-progress structure (an AST root, +/// symbol tables, ...) and an allocator on it, so parsers can build long-lived output as the +/// input is consumed -- immediate-mode parsing with no separate tree-walking pass. +/// + +/// +/// This is the design philosophy behind the macros below: +/// +/// - A parser is an ordinary `static inline` function. `PcParser` writes its signature so it +/// is steppable in a debugger; the only thing a grammar body ever calls into is another +/// parser (through `PcMatch`/`PcAlt`/...), never opaque combinator glue. +/// - `StrIter` (the input stream) and `PcParserStatus` (the result) are internal constructs. +/// They appear only inside the block frames, the arms, and the two atoms -- a grammar rule +/// built on top of these never names either one. +/// - Scannerless: parsers consume the source character-by-character through a `StrIter`; there +/// is no tokenization pass. +/// + +/// +/// Status returned by every parser. Two independent bits: whether the parse succeeded, and +/// whether any input was consumed (which stays meaningful even when the parse later fails, +/// because it decides commit-vs-backtrack in an enclosing choice). Grammar bodies never read +/// or write this directly; the macros construct and inspect it. +/// +typedef u32 PcParserStatus; + +enum { + /// The parse did not succeed. + PC_PARSER_STATUS_FAILED = 0, + /// The parse succeeded. + PC_PARSER_STATUS_SUCCESS = 1, + /// Input was advanced (matters for backtracking even on a failed parse). + PC_PARSER_STATUS_CONSUMED = 1u << 1 +}; + +/// +/// Diagnostics. Error reporting is a shared model, not per-grammar: a parser records what it has +/// to say (a level + a span into the input + its own words) and stays out of the rendering. The +/// grammar author never draws a caret or computes a column -- a renderer turns these into output. +/// Reporting does NOT unwind (it is not "raise"): a rule records a `PcReport`, substitutes a +/// placeholder/poison value, and keeps parsing, so one input yields as many errors as possible. +/// +typedef enum { + PC_REPORT_INFO, + PC_REPORT_WARN, + PC_REPORT_ERROR +} PcReportLevel; + +typedef struct PcReport { + u64 start; ///< span start, an index into the input (see `IterIndex`) + u64 end; ///< span end, exclusive + PcReportLevel level; + Zstr message; ///< the parser's words; the specifics show under the caret +} PcReport; + +typedef Vec(PcReport) PcReports; + +/// +/// PcReportsRender: draw the recorded diagnostics rustc-style into `out`. For each report it appends +/// a `level: message` line, the source line the report spans (bounded by a newline or NUL on either +/// side, so it works on the whole multi-line input), and a caret run under the span. `src` is the +/// parsed input; a grammar only records `PcReport`s and never draws a caret. The caller owns `out` +/// and decides where it goes -- print it, log it, ... -- so the renderer is I/O-free. +/// +void PcReportsRender(Str *out, Str *src, PcReports *reports); + +/// +/// The parser context and its savepoint contract +/// ============================================== +/// +/// `PcParserCtx` is NOT defined here. Each grammar (a C parser, a JSON parser, ...) declares its +/// own `typedef struct { ... } PcParserCtx;` before using `PcParser`, and it is threaded through +/// every parser as `ctx`. It carries whatever the grammar accumulates as it parses -- typically an +/// allocator, a `Vec(PcReport) reports;` sink (required by the `PcReport*` macros), and any +/// context-sensitive state the grammar discovers along the way (a symbol table, the set of typedef +/// names, ...). The combinator only forwards `ctx`; it never looks inside it. +/// +/// Because a backtracking parser abandons speculative attempts, any state a parser writes into +/// `ctx` while trying an alternative that later fails must be undone -- otherwise a rule that +/// discovered a symbol on a path the parse did not take would leave that symbol behind. The DSL +/// undoes it automatically, provided the grammar supplies a small savepoint contract next to its +/// `PcParserCtx`: +/// +/// - a type `PcParserCtxMark` an opaque saved-state handle. +/// - a func `PcParserCtxMark PcParserCtxSnapshot(PcParserCtx *ctx)` +/// capture the current context. +/// - a func `void PcParserCtxRollback(PcParserCtx *ctx, PcParserCtxMark mark)` +/// restore a previously captured +/// context. +/// +/// The grammar author writes ONLY the context mutations (bind a symbol, record a type). The author +/// never calls snapshot or rollback -- the combinators do, around every attempt that may be +/// abandoned. The division of labour is: you mutate, the DSL saves and restores. +/// +/// Contract -- what the two functions must guarantee +/// ------------------------------------------------- +/// Picture the context as a state machine with three states, relative to the most recent mark: +/// +/// SETTLED no mark outstanding; the context is the committed baseline. +/// MARKED a mark is held and the context still equals it (nothing changed since). +/// DIRTY a mark is held and the context has changed since it was taken. +/// +/// Snapshot (a mutation) +/// SETTLED ------------------> MARKED -------------------> DIRTY --. +/// ^ | | <--' (a mutation) +/// | Rollback(mark) | Rollback(mark) | +/// '-----------------------------+------------------------------' +/// +/// - Snapshot does NOT change the context; it returns a mark bound to the current state. +/// - A mutation is the whole CLASS of context-changing operations the grammar performs; every one +/// must be reversible with respect to every outstanding mark. +/// - Rollback(mark) restores the context to exactly what it was when `mark` was taken, however +/// many mutations happened since. A mark may be rolled back to more than once (an alternation +/// rewinds to the same mark once per failing arm), so rollback must be repeatable. +/// +/// How the combinators use it during backtracking +/// ---------------------------------------------- +/// Before a combinator tries an attempt it might abandon -- an arm of a `PcChoice`, the body of a +/// `PcOpt`, one round of a `PcMatchZeroOrMore`/`PcMatchOneOrMore` -- it takes a mark. If the attempt is abandoned (an arm +/// fails without committing, an optional is absent, a repetition stops), the context is rolled back +/// to that mark, so a failed attempt leaves the context exactly as it found it. If the attempt +/// commits (it consumed input and belongs to the parse), its mutations stay. A grammar therefore +/// reads and writes `ctx` as ordinary imperative code and still parses soundly under backtracking, +/// with no manual save or restore anywhere in the grammar. +/// +/// The `reports` sink is deliberately OUTSIDE this contract: diagnostics accumulate monotonically +/// and are never rolled back, so an error explaining why a branch failed survives even after the +/// parser backtracks past it. Snapshot and rollback concern the grammar's semantic state only. +/// + +/// Mangled name of the parser function for rule `Name`. +#define PcGenParserName(Name) pc_parser_##Name + +/// Portable "may be unused" attribute for the `ctx` parameter: not every parser needs the +/// context (recognizers, pure arithmetic, ...), and those should not warn. +#if defined(__GNUC__) || defined(__clang__) +# define PC_MAYBE_UNUSED __attribute__((unused)) +#else +# define PC_MAYBE_UNUSED +#endif + +/// +/// Invoke an output-producing parser by name, threading the stream `in` and context `ctx` +/// automatically. Overloaded by arity, mirroring `PcParser` -- the last argument is the output: +/// +/// PcParse(Name, Out) -> pc_parser_Name(in, ctx, Out) +/// PcParse(Name, In, Out) -> pc_parser_Name(in, ctx, In, Out) +/// +/// Used internally by `PcMatch`/`PcAlt`/`PcMatchOneOrMore`/`PcOpt`; call it directly only to delegate one +/// rule wholesale to another (`return PcParse(Other, value);`). Because an output parser always +/// carries the output argument, this family never sees an empty argument list -- so there is no +/// `__VA_OPT__` here (the recognizer family below has none either, for the same reason: every +/// arity writes its call explicitly). +/// +#define PcParse(...) OVERLOAD(PcParse, __VA_ARGS__) +#define PcParse_2(Name, Out) PcGenParserName(Name)(in, ctx, Out) +#define PcParse_3(Name, In, Out) PcGenParserName(Name)(in, ctx, In, Out) + +/// +/// The recognizer family: parsers that produce NO output -- they only succeed/fail (and consume), +/// the "validator" style. Identical to the `PcParser`/`PcParse` family with the trailing output +/// slot removed, so the arities shift down by one: 1-arg (no input) or 2-arg (an input to match). +/// +/// PcRecognizer(Name) -> (PC_ITER *in, PcParserCtx *ctx) define, no input +/// PcRecognizer(Name, InT) -> (PC_ITER *in, PcParserCtx *ctx, InT expect) define, one input +/// PcRecognize(Name) -> pc_parser_Name(in, ctx) call, no input +/// PcRecognize(Name, In) -> pc_parser_Name(in, ctx, In) call, one input +/// +/// SUCCESS: The body returns `PC_PARSER_STATUS_SUCCESS` (| `CONSUMED` when it advanced `in`). +/// FAILURE: The body returns `PC_PARSER_STATUS_FAILED` (| `CONSUMED` when it advanced then failed). +/// +#define PcRecognizer(...) OVERLOAD(PcRecognizer, __VA_ARGS__) +#define PcRecognizer_1(Name) \ + static inline PcParserStatus PcGenParserName(Name)(PC_ITER * in, PcParserCtx * ctx PC_MAYBE_UNUSED) +#define PcRecognizer_2(Name, InT) \ + static inline PcParserStatus PcGenParserName(Name)(PC_ITER * in, PcParserCtx * ctx PC_MAYBE_UNUSED, InT expect) + +#define PcRecognize(...) OVERLOAD(PcRecognize, __VA_ARGS__) +#define PcRecognize_1(Name) PcGenParserName(Name)(in, ctx) +#define PcRecognize_2(Name, In) PcGenParserName(Name)(in, ctx, In) + +/// +/// Run a parser from OUTSIDE a parser body -- the entry point a driver (a REPL, a CLI) uses to +/// kick off the top rule, so it never spells the mangled `pc_parser_` by hand. `PcParse` +/// threads the ambient stream `in` for a rule; a driver owns its own stream, so it passes a +/// pointer to it explicitly. `ctx` is still taken from the enclosing scope. The remaining args +/// (the outputs) forward to the parser call. +/// +/// PcRun(Name, &in, &out) -> pc_parser_Name(&in, ctx, &out) +/// +#define PcRun(Name, InPtr, ...) PcGenParserName(Name)(InPtr, ctx, __VA_ARGS__) + +/// +/// Define (or, when followed by `;`, forward-declare) a parser. Overloaded by arity: +/// +/// PcParser(Name, BuildT) -> (PC_ITER *in, PcParserCtx *ctx, BuildT *value) +/// PcParser(Name, InT, BuildT) -> (PC_ITER *in, PcParserCtx *ctx, InT expect, BuildT *value) +/// +/// The names a body reads are `in` (stream), `ctx` (grammar context), `expect` (the input, in +/// the 3-arg form), and `value` (the output). The consumer must have a `PcParserCtx` type in scope. +/// +/// SUCCESS: The body returns `PC_PARSER_STATUS_SUCCESS` (or'd with `CONSUMED` when it advanced +/// `in`); the parsed result has been written to `*value`. +/// FAILURE: The body returns `PC_PARSER_STATUS_FAILED` (or'd with `CONSUMED` when it advanced +/// before failing); `*value` is left unspecified. +/// +#define PcParser(...) OVERLOAD(PcParser, __VA_ARGS__) +#define PcParser_2(Name, BuildT) \ + static inline PcParserStatus PcGenParserName(Name)(PC_ITER * in, PcParserCtx * ctx PC_MAYBE_UNUSED, BuildT * value) +#define PcParser_3(Name, InT, BuildT) \ + static inline PcParserStatus \ + PcGenParserName(Name)(PC_ITER * in, PcParserCtx * ctx PC_MAYBE_UNUSED, InT expect, BuildT * value) + +/// +/// The consumed bit for a parser, derived from the stream position against a snapshot: a parse +/// that rewound leaves `pos` unchanged (not consumed); one that committed leaves it advanced +/// (consumed). This is why backtracking accounts for itself -- no separate flag is threaded. +/// +#define PC_CONSUMED(start) (in->pos != (start).pos ? PC_PARSER_STATUS_CONSUMED : 0u) + +/// +/// Block frames -- the whole body of a rule is one of these (or a bare `PcSatisfy*`/delegation). +/// Each scopes its bookkeeping in the `for`-init struct so frames nest and sit side by side +/// without name clashes, and each owns the rule's `return`. +/// +/// PcSeq: run the steps (`PcMatch`/`PcMatchOneOrMore`/...) in order; a failing step returns early. If the +/// body runs to the end, the rule succeeds (consuming whatever the steps consumed). +/// +/// SUCCESS: All steps matched; returns SUCCESS with the accumulated CONSUMED bit. +/// FAILURE: A step failed and already returned; control never reaches the frame's success return. +/// +#define PcSeq() \ + for (struct { \ + PC_ITER start; \ + bool ran; \ + } pc_seq = {*in, false}; \ + ; \ + pc_seq.ran = true) \ + if (pc_seq.ran) \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_SUCCESS; \ + else + +/// +/// PcChoice: try the arms (`PcAlt`/`PcTryAlt`/...) top to bottom; the first that matches wins and +/// its value is the rule's value. If none match, the rule fails. +/// +/// SUCCESS: An arm matched; returns SUCCESS with CONSUMED reflecting the arm. +/// FAILURE: No arm matched; returns FAILED, CONSUMED set if the rule advanced before failing +/// (a committed arm that consumed then failed) -- driving the caller's commit-vs-backtrack. +/// +#define PcChoice() \ + for (struct { \ + PC_ITER mark; \ + PcParserCtxMark ctx_mark; \ + PcParserStatus st; \ + bool ran, matched, done; \ + } pc_ch = {*in, PcParserCtxSnapshot(ctx), 0, false, false, false}; \ + ; \ + pc_ch.ran = true) \ + if (pc_ch.ran) \ + return (pc_ch.matched ? PC_PARSER_STATUS_SUCCESS : PC_PARSER_STATUS_FAILED) | PC_CONSUMED(pc_ch.mark); \ + else + +/// +/// Sequence steps (used inside `PcSeq`). Both forward their extra args straight to the parser +/// call, so the parser's own signature validates arity and types at the call site. +/// +/// PcMatch: run parser `Name`; on failure, fail the whole rule (returning with the sequence's +/// consumed bit). On success, continue with the next step. +/// +#define PcMatch(Name, ...) \ + do { \ + if (!(PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS)) \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ + } while (0) + +/// +/// PcReject: fail the current rule outright from its body -- the escape hatch for a +/// context-sensitive rejection no combinator can express (an undefined variable, a name that is +/// not a typedef, ...). Reports the sequence's consumed bit, exactly as a failing `PcMatch` would, +/// so an enclosing choice commits/backtracks correctly. A `PcSeq` step, like `PcMatch`; a rule +/// uses it so it never has to name the consumed bit or the status itself. +/// +#define PcReject() return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED + +/// +/// PcFailIfNotEof: a `PcSeq` step that succeeds only at end of input. If any input remains, it +/// records `Msg` at the cursor and fails the rule (exactly as `PcReject`, carrying the sequence's +/// consumed bit) -- the "the whole input had to parse" assertion, so a grammar never inspects the +/// cursor by hand. `ctx` must carry a `Vec(PcReport) reports;` sink for the recorded cause. +/// +#define PcFailIfNotEof(Msg) \ + do { \ + char UNPL(pc_eof_) = 0; \ + if (IterPeekAt(in, 0, &UNPL(pc_eof_))) { \ + PcReportErrorHere(Msg); \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ + } \ + } while (0) + +/// +/// Record a diagnostic and KEEP GOING -- the greedy alternative to `PcReject`. Appends a +/// `PcReport` (spanning what the current `PcSeq` frame has consumed so far) to `ctx->reports`, +/// then returns nothing, so the rule substitutes a poison value and parsing continues to collect +/// more errors. `ctx` must carry a `Vec(PcReport) reports;`. A `PcSeq` step, like `PcReject`. +/// +#define PcReportError(Msg) PC_REPORT(PC_REPORT_ERROR, Msg) +#define PcReportWarn(Msg) PC_REPORT(PC_REPORT_WARN, Msg) +#define PcReportInfo(Msg) PC_REPORT(PC_REPORT_INFO, Msg) +#define PC_REPORT(Level, Msg) \ + VecPushBackR( \ + &ctx->reports, \ + ((PcReport) {.start = (pc_seq.start).pos, .end = IterIndex(in), .level = (Level), .message = (Msg)}) \ + ) + +/// +/// The `...Here` variants report a diagnostic whose span is the single character at the current +/// cursor, rather than the enclosing `PcSeq` frame. Use them where nothing has been consumed and +/// there is no frame span to point at -- an "expected X"-style error, e.g. inside a `PcChoice` +/// where every arm has rewound. They read only the cursor, so they need no `pc_seq`. +/// +#define PcReportErrorHere(Msg) PC_REPORT_HERE(PC_REPORT_ERROR, Msg) +#define PcReportWarnHere(Msg) PC_REPORT_HERE(PC_REPORT_WARN, Msg) +#define PcReportInfoHere(Msg) PC_REPORT_HERE(PC_REPORT_INFO, Msg) +#define PC_REPORT_HERE(Level, Msg) \ + VecPushBackR( \ + &ctx->reports, \ + ((PcReport) {.start = IterIndex(in), .end = IterIndex(in) + 1, .level = (Level), .message = (Msg)}) \ + ) + +/// +/// PcMatchZeroOrMore: zero-or-more ("any number", including none). Run parser `Name` repeatedly; the body runs +/// once per match. A match that consumed nothing would spin forever, so the loop stops on it (it +/// never aborts and never allocates); the failing/empty iteration is rewound so its bytes are not +/// eaten. Always "succeeds" -- it is a step that simply stops. +/// +#define PcMatchZeroOrMore(Name, ...) \ + for (struct { \ + PC_ITER mark; \ + PcParserCtxMark ctx_mark; \ + } UNPL(pc_zom_) = {*in, PcParserCtxSnapshot(ctx)}; \ + (UNPL(pc_zom_).mark = *in, \ + UNPL(pc_zom_).ctx_mark = PcParserCtxSnapshot(ctx), \ + (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_zom_).mark.pos) ? \ + true : \ + (*in = UNPL(pc_zom_).mark, PcParserCtxRollback(ctx, UNPL(pc_zom_).ctx_mark), false);) + +/// +/// PcMatchOneOrMore: one-or-more. Like `PcMatchZeroOrMore`, but at least one match is required: if the first attempt does +/// not match, the whole rule fails (reporting the sequence's consumed bit, exactly as a failing +/// `PcMatch` would), so it is a `PcSeq` step. The body runs once per match, the first included -- +/// folding the common "match one, then any more" shape into a single step. +/// +#define PcMatchOneOrMore(Name, ...) \ + for (struct { \ + PC_ITER mark; \ + PcParserCtxMark ctx_mark; \ + bool done; \ + } UNPL(pc_oom1_) = {*in, PcParserCtxSnapshot(ctx), false}; \ + !UNPL(pc_oom1_).done; \ + UNPL(pc_oom1_).done = true) \ + if ((UNPL(pc_oom1_).mark = *in, \ + UNPL(pc_oom1_).ctx_mark = PcParserCtxSnapshot(ctx), \ + !((PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_oom1_).mark.pos))) \ + return ( \ + *in = UNPL(pc_oom1_).mark, \ + PcParserCtxRollback(ctx, UNPL(pc_oom1_).ctx_mark), \ + PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED \ + ); \ + else \ + for (struct { \ + PC_ITER mark; \ + PcParserCtxMark ctx_mark; \ + bool first; \ + } UNPL(pc_oomN_) = {*in, PcParserCtxSnapshot(ctx), true}; \ + UNPL(pc_oomN_).first ? \ + true : \ + (UNPL(pc_oomN_).mark = *in, \ + UNPL(pc_oomN_).ctx_mark = PcParserCtxSnapshot(ctx), \ + (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_oomN_).mark.pos ? \ + true : \ + (*in = UNPL(pc_oomN_).mark, PcParserCtxRollback(ctx, UNPL(pc_oomN_).ctx_mark), false)); \ + UNPL(pc_oomN_).first = false) + +/// +/// PcOpt: zero-or-one. Try parser `Name`; if it matches, run the body once (the parsed value is +/// available through whatever output pointer was passed). If it does not match, rewind and skip +/// the body. Never fails the sequence -- it is a step that is simply optional. Like `PcMatchOneOrMore`, it +/// is one `for` statement with its bookkeeping in the loop scope, so it nests, sits next to other +/// steps on the same line, and takes an unbraced body without a dangling-`else` surprise. +/// +#define PcOpt(Name, ...) \ + for (struct { \ + PC_ITER mark; \ + PcParserCtxMark ctx_mark; \ + bool ran; \ + } UNPL(pc_opt_) = {*in, PcParserCtxSnapshot(ctx), false}; \ + !UNPL(pc_opt_).ran && \ + ((UNPL(pc_opt_).ran = true), \ + (PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS) ? \ + true : \ + (*in = UNPL(pc_opt_).mark, PcParserCtxRollback(ctx, UNPL(pc_opt_).ctx_mark), false));) + +/// +/// PcRecognizeZeroOrMore: the recognizer-flavoured `PcMatchZeroOrMore` -- a whole recognizer body that runs a +/// sub-recognizer zero-or-more times ("any number", including none) and then returns success. +/// Terminal (it owns the `return`), so it is the entire body of a "recognize zero-or-more of one +/// thing" parser (e.g. whitespace), not a mid-sequence step. A non-consuming match stops the loop +/// and is rewound, so it can never spin. Overloaded 1-arg / 2-arg like `PcRecognize`. +/// +#define PcRecognizeZeroOrMore(...) OVERLOAD(PcRecognizeZeroOrMore, __VA_ARGS__) +#define PcRecognizeZeroOrMore_1(Name) PC_RECOGNIZE_REPEAT(PcRecognize_1(Name), 0) +#define PcRecognizeZeroOrMore_2(Name, In) PC_RECOGNIZE_REPEAT(PcRecognize_2(Name, In), 0) + +/// +/// PcRecognizeOneOrMore: the one-or-more counterpart of `PcRecognizeZeroOrMore` -- the whole body of a +/// "recognize one-or-more of one thing" recognizer. Identical, except it returns failure when it +/// matched nothing at all. Terminal, like `PcRecognizeZeroOrMore`. +/// +#define PcRecognizeOneOrMore(...) OVERLOAD(PcRecognizeOneOrMore, __VA_ARGS__) +#define PcRecognizeOneOrMore_1(Name) PC_RECOGNIZE_REPEAT(PcRecognize_1(Name), 1) +#define PcRecognizeOneOrMore_2(Name, In) PC_RECOGNIZE_REPEAT(PcRecognize_2(Name, In), 1) +#define PC_RECOGNIZE_REPEAT(call, min_one) \ + do { \ + PC_ITER UNPL(pc_rm_start) = *in; \ + for (struct { \ + PC_ITER mark; \ + PcParserCtxMark ctx_mark; \ + } UNPL(pc_rm_) = {*in, PcParserCtxSnapshot(ctx)}; \ + (UNPL(pc_rm_).mark = *in, \ + UNPL(pc_rm_).ctx_mark = PcParserCtxSnapshot(ctx), \ + ((call) & PC_PARSER_STATUS_SUCCESS) && in->pos != UNPL(pc_rm_).mark.pos) ? \ + true : \ + (*in = UNPL(pc_rm_).mark, PcParserCtxRollback(ctx, UNPL(pc_rm_).ctx_mark), false);) \ + ; \ + if ((min_one) && in->pos == UNPL(pc_rm_start).pos) \ + return PC_PARSER_STATUS_FAILED; \ + return PC_CONSUMED(UNPL(pc_rm_start)) | PC_PARSER_STATUS_SUCCESS; \ + } while (0) + +/// +/// Choice arms (used inside `PcChoice`). All forward their extra args to the parser call. +/// +/// `PcAlt`/`PcTryAlt` are BODILESS: the matched parser's output IS the arm's value. Each is a +/// complete statement, so writing a `{ ... }` after one is a compile error. +/// `PcAltThen`/`PcTryAltThen` take a build body, run on a match, to transform the parsed value. +/// +/// Plain arms COMMIT: if the arm consumed input then failed, the choice fails (consumed) rather +/// than trying later arms. `Try` arms BACKTRACK: any failure rewinds and the next arm is tried. +/// +#define PcAlt(Name, ...) \ + ((void)(pc_ch.done || ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + ((pc_ch.st & PC_PARSER_STATUS_CONSUMED) ? \ + (pc_ch.done = true, false) : \ + (*in = pc_ch.mark, PcParserCtxRollback(ctx, pc_ch.ctx_mark), false))))) + +#define PcTryAlt(Name, ...) \ + ((void)(pc_ch.done || ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + (*in = pc_ch.mark, PcParserCtxRollback(ctx, pc_ch.ctx_mark), false)))) + +#define PcAltThen(Name, ...) \ + if (!pc_ch.done && ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + ((pc_ch.st & PC_PARSER_STATUS_CONSUMED) ? \ + (pc_ch.done = true, false) : \ + (*in = pc_ch.mark, PcParserCtxRollback(ctx, pc_ch.ctx_mark), false)))) + +#define PcTryAltThen(Name, ...) \ + if (!pc_ch.done && ((pc_ch.st = PcParse(Name, __VA_ARGS__)) & PC_PARSER_STATUS_SUCCESS ? \ + ((pc_ch.matched = pc_ch.done = true)) : \ + (*in = pc_ch.mark, PcParserCtxRollback(ctx, pc_ch.ctx_mark), false))) + +/// +/// PcElse: the fallback arm of a `PcChoice`. Its body runs iff NO arm matched (every arm failed +/// cleanly, consuming nothing), and it marks the choice handled so the rule succeeds with whatever +/// the body writes to the output (typically a report plus a poison value). Put it last. A rule +/// uses it so it never has to name `pc_ch`. +/// +#define PcElse() if (!pc_ch.done && (pc_ch.matched = pc_ch.done = true)) + +/// +/// PcRecover: recovery skip. Advance the stream until the current character satisfies `IsSync` (a +/// resynchronization boundary) or the stream ends; `Var` is the loop-scoped peeked char. Pairs +/// with a report and a poison value to recover from a syntax error and carry on, so one input +/// surfaces more than just the first structural break. +/// +#define PcRecover(Var, IsSync) \ + for (char Var = 0; IterPeekAt(in, 0, &Var) && !(IsSync);) \ + IterMove(in, 1) + +/// +/// PcCaptureUntil: capture the run of input from the cursor up to (not including) the first char +/// satisfying `IsStop`, or end of input. Binds `PtrOut` to a `Zstr` at the run's start and `LenOut` +/// (a `u64`) to its byte length -- a borrowed `(ptr, len)` view the rule does what it wants with: +/// parse it to a number, copy it into an owned `Str`, or store it as-is. The DSL owns the cursor the +/// whole time, so the rule never touches `in`. `Var` is the loop-scoped peeked char (as in +/// `PcRecover`). The cursor is left AT the stop char -- consume it separately if the grammar must +/// move past it. +/// +#define PcCaptureUntil(Var, IsStop, PtrOut, LenOut) \ + do { \ + u64 UNPL(pc_cap_) = IterIndex(in); \ + for (char Var = 0; IterPeekAt(in, 0, &Var) && !(IsStop);) \ + IterMove(in, 1); \ + *(PtrOut) = (Zstr)IterDataAt(in, UNPL(pc_cap_)); \ + *(LenOut) = IterIndex(in) - UNPL(pc_cap_); \ + } while (0) + +/// +/// Atoms -- the only place `StrIter` and `PcParserStatus` are handled directly. Every fundamental +/// parser (a char class, a keyword, ...) is written on top of one of these, so it never pokes the +/// stream or builds a status by hand. +/// +/// PcSatisfyChar: match one char, bound as `Var`, for which `Pred` holds; run the build body, +/// then succeed (consuming the char). If `Pred` is false at the current position, fail without +/// consuming. `Var` is loop-scoped (it does not leak into the parser body). +/// +/// SUCCESS: `Pred(Var)` held; the char is consumed, the body has run; returns SUCCESS|CONSUMED. +/// FAILURE: `Pred(Var)` did not hold (or there was no char); nothing consumed; returns FAILED. +/// +#define PcSatisfyChar(Var, Pred) \ + for (char Var = 0;;) \ + for (bool UNPL(pc_sc_ran) = false;; UNPL(pc_sc_ran) = true) \ + if (UNPL(pc_sc_ran)) \ + return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ + else if (!(IterPeekAt(in, 0, &Var) && (Pred))) \ + return PC_PARSER_STATUS_FAILED; \ + else if ((IterMove(in, 1), true)) + +/// +/// PcSatisfyStr: match the literal string `Expect` (a `Zstr`; pass `StrBegin(&s)` for a `Str`) at +/// the current position. The whole string is peeked before committing, so a mismatch consumes +/// nothing (a clean failure, usable in a choice without a commit surprise); a full match consumes +/// it and runs the build body. +/// +/// SUCCESS: `Expect` was present; it is consumed, the body has run; returns SUCCESS|CONSUMED. +/// FAILURE: `Expect` was not present; nothing consumed; returns FAILED. +/// +#define PcSatisfyStr(Expect) \ + Zstr UNPL(pc_ss_exp) = (Expect); \ + u64 UNPL(pc_ss_len) = ZstrLen(UNPL(pc_ss_exp)); \ + bool UNPL(pc_ss_ok) = true; \ + for (u64 UNPL(pc_ss_i) = 0; UNPL(pc_ss_i) < UNPL(pc_ss_len); UNPL(pc_ss_i)++) { \ + char UNPL(pc_ss_c); \ + if (!IterPeekAt(in, (i64)UNPL(pc_ss_i), &UNPL(pc_ss_c)) || UNPL(pc_ss_c) != UNPL(pc_ss_exp)[UNPL(pc_ss_i)]) { \ + UNPL(pc_ss_ok) = false; \ + break; \ + } \ + } \ + if (UNPL(pc_ss_ok)) \ + IterMustMove(in, (i64)UNPL(pc_ss_len)); \ + for (bool UNPL(pc_ss_ran) = false;; UNPL(pc_ss_ran) = true) \ + if (!UNPL(pc_ss_ok)) \ + return PC_PARSER_STATUS_FAILED; \ + else if (UNPL(pc_ss_ran)) \ + return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ + else + +/// +/// Binary fields -- for a grammar with `#define PC_ITER BufIter`. The fixed-width readers are NOT +/// written per grammar: `PcU8`/`PcI8`, `Pc{U,I}16BE`/`LE`, `Pc{U,I}32BE`/`LE`, `Pc{U,I}64BE`/`LE` +/// are fundamental parsers delivered out of the box (declared here, defined in ParserCombinator.c) +/// and used through `PcMatch` / `PcAlt` -- e.g. `PcMatch(PcI32BE, &field)`. Each reads one field +/// from the cursor at the stated endianness and signedness; a short buffer fails the rule. Magic is +/// `PcSatisfyStr`, not a reader. +/// +/// The delivered parsers live outside the grammar's translation unit, so they see the context only +/// as an opaque `struct PcParserCtx` (which they ignore) -- so a grammar MUST define its context as +/// a tagged `typedef struct PcParserCtx { ... } PcParserCtx;`, so the opaque pointer lines up. +/// +struct PcParserCtx; +PcParserStatus pc_parser_PcU8(BufIter *in, struct PcParserCtx *ctx, u8 *value); +PcParserStatus pc_parser_PcU16BE(BufIter *in, struct PcParserCtx *ctx, u16 *value); +PcParserStatus pc_parser_PcU16LE(BufIter *in, struct PcParserCtx *ctx, u16 *value); +PcParserStatus pc_parser_PcU32BE(BufIter *in, struct PcParserCtx *ctx, u32 *value); +PcParserStatus pc_parser_PcU32LE(BufIter *in, struct PcParserCtx *ctx, u32 *value); +PcParserStatus pc_parser_PcU64BE(BufIter *in, struct PcParserCtx *ctx, u64 *value); +PcParserStatus pc_parser_PcU64LE(BufIter *in, struct PcParserCtx *ctx, u64 *value); +PcParserStatus pc_parser_PcI8(BufIter *in, struct PcParserCtx *ctx, i8 *value); +PcParserStatus pc_parser_PcI16BE(BufIter *in, struct PcParserCtx *ctx, i16 *value); +PcParserStatus pc_parser_PcI16LE(BufIter *in, struct PcParserCtx *ctx, i16 *value); +PcParserStatus pc_parser_PcI32BE(BufIter *in, struct PcParserCtx *ctx, i32 *value); +PcParserStatus pc_parser_PcI32LE(BufIter *in, struct PcParserCtx *ctx, i32 *value); +PcParserStatus pc_parser_PcI64BE(BufIter *in, struct PcParserCtx *ctx, i64 *value); +PcParserStatus pc_parser_PcI64LE(BufIter *in, struct PcParserCtx *ctx, i64 *value); + +/// +/// PC_BYTE_ATOM: the terminal body of a fundamental byte reader (the delivered parsers and +/// `PcSkipBytes`) -- read/advance or fail the rule; on success return success|consumed. +/// +#define PC_BYTE_ATOM(call) \ + do { \ + if (!(call)) \ + return PC_PARSER_STATUS_FAILED; \ + return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ + } while (0) + +/// +/// PcSkipBytes: advance `N` bytes; a short buffer fails. TERMINAL, the body of a recognizer. +/// +#define PcSkipBytes(N) PC_BYTE_ATOM(IterMove(in, (i64)(N))) + +/// +/// PcExpect: a `PcSeq` step that runs a RECOGNIZER (no output) and fails the rule if it did not +/// match -- the recognizer twin of `PcMatch`, for fixed markers (a magic recognizer, a skip). +/// +#define PcExpect(...) \ + do { \ + if (!(PcRecognize(__VA_ARGS__) & PC_PARSER_STATUS_SUCCESS)) \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ + } while (0) + +/// +/// PcMatchExactlyN: run parser `Name` exactly `N` times as a `PcSeq` step, binding `Idx` (a `u64`) +/// to the iteration index for the body to read -- the counted, index-bearing sibling of +/// `PcMatchZeroOrMore` (as `VecForeachIdx` is to `VecForeach`). Any of the `N` failing fails the +/// rule (with the sequence's consumed bit). The body runs after each match. +/// +#define PcMatchExactlyN(N, Idx, Name, ...) \ + for (u64 Idx = 0; Idx < (u64)(N); Idx++) \ + if (!(PcParse(Name, __VA_ARGS__) & PC_PARSER_STATUS_SUCCESS)) \ + return PC_CONSUMED(pc_seq.start) | PC_PARSER_STATUS_FAILED; \ + else + +/// +/// PcRecognizeExactlyN: run recognizer `Name` exactly `N` times -- the counted sibling of +/// `PcRecognizeZeroOrMore`. TERMINAL (the whole recognizer body); any failure fails. +/// +#define PcRecognizeExactlyN(...) OVERLOAD(PcRecognizeExactlyN, __VA_ARGS__) +#define PcRecognizeExactlyN_2(N, Name) PC_RECOGNIZE_EXACTLY((N), PcRecognize_1(Name)) +#define PcRecognizeExactlyN_3(N, Name, In) PC_RECOGNIZE_EXACTLY((N), PcRecognize_2(Name, In)) +#define PC_RECOGNIZE_EXACTLY(N, call) \ + do { \ + for (u64 UNPL(pc_rxn_) = 0; UNPL(pc_rxn_) < (u64)(N); UNPL(pc_rxn_)++) \ + if (!((call) & PC_PARSER_STATUS_SUCCESS)) \ + return PC_PARSER_STATUS_FAILED; \ + return PC_PARSER_STATUS_SUCCESS | PC_PARSER_STATUS_CONSUMED; \ + } while (0) + +#endif // MISRA_PARSER_COMBINATOR diff --git a/Include/Misra/Parsers/Elf.h b/Include/Misra/Parsers/Elf.h index 50996e3..e24cf4d 100644 --- a/Include/Misra/Parsers/Elf.h +++ b/Include/Misra/Parsers/Elf.h @@ -243,10 +243,18 @@ typedef struct Elf { /// /// Open and parse an ELF file from disk. /// -/// out[out] : Populated on success. -/// path[in] : Filesystem path. Prefer `Str *`; `Zstr` (NUL-terminated) accepted. -/// alloc[in] : Allocator for the read-in byte buffer and the section / -/// symbol vectors. Must outlive the `Elf`. +/// Call shapes via `OVERLOAD` + `_Generic` on `path`: +/// `ElfOpen(out, path)` -- `path` is `Str *` or `Zstr`. +/// `ElfOpen(out, path, alloc)` -- same, explicit allocator. +/// `ElfOpen(out, path, path_len, alloc)`-- `path` is a fixed-length view +/// (`Zstr`, `size`); copied into a +/// stack buffer for the syscall. +/// +/// out[out] : Populated on success. +/// path[in] : Filesystem path. Prefer `Str *`; `Zstr` (NUL-terminated) accepted. +/// path_len[in] : Length of `path` for the fixed-length form. +/// alloc[in] : Allocator for the read-in byte buffer and the section / +/// symbol vectors. Must outlive the `Elf`. /// /// SUCCESS : Returns true; `out` owns the read-in buffer and will free /// it on `ElfDeinit`. @@ -270,6 +278,7 @@ typedef struct Elf { Zstr: elf_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: elf_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define ElfOpen_4(out, path, len, alloc) elf_open_n((out), (Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Parse an ELF object from an in-memory byte range -- **L-value / diff --git a/Include/Misra/Parsers/Elf/Private.h b/Include/Misra/Parsers/Elf/Private.h index c82859f..27a58ba 100644 --- a/Include/Misra/Parsers/Elf/Private.h +++ b/Include/Misra/Parsers/Elf/Private.h @@ -19,13 +19,14 @@ extern "C" { #endif -typedef struct Elf Elf; -typedef struct ElfSection ElfSection; + typedef struct Elf Elf; + typedef struct ElfSection ElfSection; -bool elf_open(Elf *out, Zstr path, Allocator *alloc); -bool elf_open_from_memory_copy(Elf *out, const u8 *data, size data_size, Allocator *alloc); -const ElfSection *elf_find_section_zstr(const Elf *self, Zstr name); -const ElfSection *elf_find_section_str(const Elf *self, const Str *name); + bool elf_open(Elf *out, Zstr path, Allocator *alloc); + bool elf_open_n(Elf *out, Zstr path, size len, Allocator *alloc); + bool elf_open_from_memory_copy(Elf *out, const u8 *data, size data_size, Allocator *alloc); + const ElfSection *elf_find_section_zstr(const Elf *self, Zstr name); + const ElfSection *elf_find_section_str(const Elf *self, const Str *name); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/Http.h b/Include/Misra/Parsers/Http.h index 6276f31..c1d0daa 100644 --- a/Include/Misra/Parsers/Http.h +++ b/Include/Misra/Parsers/Http.h @@ -80,10 +80,16 @@ void HttpHeaderDeinit(HttpHeader *header); /// TAGS: Http, Deinit, Header, Init /// /// -/// Find a header by key (case-sensitive zero-terminated comparison). +/// Find a header by key (case-sensitive comparison). +/// +/// Two call shapes via `OVERLOAD` + `_Generic` on `key`: +/// `HttpHeadersFind(headers, key)` -- `key` is `Str *` / `Zstr`. +/// `HttpHeadersFind(headers, key, key_len)` -- `key` is a counted view +/// (`Zstr`, `size`). /// /// headers[in] : Caller's `Vec(HttpHeader)` to search. /// key[in] : Key to look up. +/// key_len[in] : Length of `key` for the 3-arg counted form. /// /// SUCCESS : Returns a pointer to the matching header inside the /// vector. The pointer is valid until `*headers` is mutated @@ -95,11 +101,14 @@ void HttpHeaderDeinit(HttpHeader *header); /// HttpHeader *http_headers_find_zstr(HttpHeaders *headers, Zstr key); HttpHeader *http_headers_find_str(HttpHeaders *headers, const Str *key); -#define HttpHeadersFind(headers, key) \ +HttpHeader *http_headers_find_cstr(HttpHeaders *headers, Zstr key, size key_len); +#define HttpHeadersFind(...) OVERLOAD(HttpHeadersFind, __VA_ARGS__) +#define HttpHeadersFind_2(headers, key) \ _Generic((key), Str *: http_headers_find_str, Zstr: http_headers_find_zstr, char *: http_headers_find_zstr)( \ (headers), \ (key) \ ) +#define HttpHeadersFind_3(headers, key, key_len) http_headers_find_cstr((headers), (Zstr)(key), (key_len)) typedef enum HttpResponseCode { HTTP_RESPONSE_CODE_INVALID = 0, @@ -254,19 +263,26 @@ typedef struct HttpRequest { /// be initialized with `HttpRequestInit(...)` so the parser has an /// allocator to write into. /// +/// Two call shapes via `OVERLOAD` + `_Generic` on `in`: +/// `HttpRequestParse(req, in)` -- `in` is `Str *` / `Zstr`. +/// `HttpRequestParse(req, in, in_len)` -- `in` is a counted view +/// (`Zstr`, `size`). +/// /// SUCCESS : Returns a pointer past the parsed request line + headers -/// (start of the body). +/// (start of the body), pointing into the caller's `in`. /// FAILURE : Returns `in` unchanged when the input is malformed. /// /// TAGS: Http, Parse, Request /// -#define HttpRequestParse(req, in) \ +#define HttpRequestParse(...) OVERLOAD(HttpRequestParse, __VA_ARGS__) +#define HttpRequestParse_2(req, in) \ _Generic( \ (in), \ Str *: http_request_parse_str((req), (const Str *)(in)), \ Zstr: http_request_parse_zstr((req), (Zstr)(in)), \ char *: http_request_parse_zstr((req), (Zstr)(in)) \ ) +#define HttpRequestParse_3(req, in, in_len) http_request_parse_cstr((req), (Zstr)(in), (in_len)) /// /// Release storage owned by `req` and zero the struct. Safe to call on @@ -312,7 +328,7 @@ typedef struct HttpResponse { ((HttpResponse) {.allocator = ALLOCATOR_OF(alloc_ptr), \ .content_type = HTTP_CONTENT_TYPE_INVALID, \ .status_code = HTTP_RESPONSE_CODE_INVALID, \ - .headers = VecInitWithDeepCopy_3(NULL, http_header_deinit, alloc_ptr), \ + .headers = VecInitWithDeepCopy_3(NULL, http_header_deinit, alloc_ptr), \ .body = StrInit_1(alloc_ptr)}) /// @@ -347,18 +363,27 @@ HttpResponse *HttpRespondWithHtml(HttpResponse *response, HttpResponseCode statu /// through `response->allocator`. Only available when the `file` /// feature is enabled. /// +/// Two call shapes via `OVERLOAD` + `_Generic` on `filepath`: +/// `HttpRespondWithFile(response, status, content_type, filepath)` +/// -- `filepath` is `Str *` / `Zstr`. +/// `HttpRespondWithFile(response, status, content_type, filepath, filepath_len)` +/// -- `filepath` is a counted view (`Zstr`, `size`). +/// /// SUCCESS : Returns `response` with body filled. /// FAILURE : Returns NULL on I/O or allocation failure. /// /// TAGS: Http, Respond, File /// -# define HttpRespondWithFile(response, status, content_type, filepath) \ +# define HttpRespondWithFile(...) OVERLOAD(HttpRespondWithFile, __VA_ARGS__) +# define HttpRespondWithFile_4(response, status, content_type, filepath) \ _Generic( \ (filepath), \ Str *: http_respond_with_file_str((response), (status), (content_type), (const Str *)(filepath)), \ Zstr: http_respond_with_file_zstr((response), (status), (content_type), (Zstr)(filepath)), \ char *: http_respond_with_file_zstr((response), (status), (content_type), (Zstr)(filepath)) \ ) +# define HttpRespondWithFile_5(response, status, content_type, filepath, filepath_len) \ + http_respond_with_file_cstr((response), (status), (content_type), (Zstr)(filepath), (filepath_len)) #endif /// diff --git a/Include/Misra/Parsers/Http/Private.h b/Include/Misra/Parsers/Http/Private.h index 9dd5c5d..e7b222d 100644 --- a/Include/Misra/Parsers/Http/Private.h +++ b/Include/Misra/Parsers/Http/Private.h @@ -21,30 +21,38 @@ extern "C" { #endif -typedef struct HttpRequest HttpRequest; -typedef struct HttpResponse HttpResponse; -typedef enum HttpResponseCode HttpResponseCode; -typedef enum HttpContentType HttpContentType; + typedef struct HttpRequest HttpRequest; + typedef struct HttpResponse HttpResponse; + typedef enum HttpResponseCode HttpResponseCode; + typedef enum HttpContentType HttpContentType; -void http_header_deinit(void *header, const Allocator *alloc); -bool http_header_init_copy(void *dst, const void *src, const Allocator *alloc); -Zstr http_request_parse_zstr(HttpRequest *req, Zstr in); -Zstr http_request_parse_str(HttpRequest *req, const Str *in); + void http_header_deinit(void *header, const Allocator *alloc); + bool http_header_init_copy(void *dst, const void *src, const Allocator *alloc); + Zstr http_request_parse_zstr(HttpRequest *req, Zstr in); + Zstr http_request_parse_str(HttpRequest *req, const Str *in); + Zstr http_request_parse_cstr(HttpRequest *req, Zstr in, size in_len); #if FEATURE_FILE -HttpResponse *http_respond_with_file_zstr( - HttpResponse *response, - HttpResponseCode status, - HttpContentType content_type, - Zstr filepath -); -HttpResponse *http_respond_with_file_str( - HttpResponse *response, - HttpResponseCode status, - HttpContentType content_type, - const Str *filepath -); + HttpResponse *http_respond_with_file_zstr( + HttpResponse *response, + HttpResponseCode status, + HttpContentType content_type, + Zstr filepath + ); + HttpResponse *http_respond_with_file_str( + HttpResponse *response, + HttpResponseCode status, + HttpContentType content_type, + const Str *filepath + ); + HttpResponse *http_respond_with_file_cstr( + HttpResponse *response, + HttpResponseCode status, + HttpContentType content_type, + Zstr filepath, + size filepath_len + ); #endif -Str http_response_serialize(const HttpResponse *response, Allocator *alloc); + Str http_response_serialize(const HttpResponse *response, Allocator *alloc); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/KvConfig.h b/Include/Misra/Parsers/KvConfig.h index 2c0586d..0cd5a97 100644 --- a/Include/Misra/Parsers/KvConfig.h +++ b/Include/Misra/Parsers/KvConfig.h @@ -216,8 +216,10 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, API /// -#define KvConfigGet(cfg, key) \ +#define KvConfigGet(...) OVERLOAD(KvConfigGet, __VA_ARGS__) +#define KvConfigGet_2(cfg, key) \ _Generic((key), Str *: kvconfig_get_str, Zstr: kvconfig_get_zstr, char *: kvconfig_get_zstr)((cfg), (key)) +#define KvConfigGet_3(cfg, key, key_len) kvconfig_get_cstr((cfg), (Zstr)(key), (key_len)) /// /// Get stored value for `key` by internal reference. @@ -230,11 +232,13 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, Pointer /// -#define KvConfigGetPtr(cfg, key) \ +#define KvConfigGetPtr(...) OVERLOAD(KvConfigGetPtr, __VA_ARGS__) +#define KvConfigGetPtr_2(cfg, key) \ _Generic((key), Str *: kvconfig_get_ptr_str, Zstr: kvconfig_get_ptr_zstr, char *: kvconfig_get_ptr_zstr)( \ (cfg), \ (key) \ ) +#define KvConfigGetPtr_3(cfg, key, key_len) kvconfig_get_ptr_cstr((cfg), (Zstr)(key), (key_len)) /// /// Check whether a key exists in config. @@ -247,11 +251,13 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Contains, API /// -#define KvConfigContains(cfg, key) \ +#define KvConfigContains(...) OVERLOAD(KvConfigContains, __VA_ARGS__) +#define KvConfigContains_2(cfg, key) \ _Generic((key), Str *: kvconfig_contains_str, Zstr: kvconfig_contains_zstr, char *: kvconfig_contains_zstr)( \ (cfg), \ (key) \ ) +#define KvConfigContains_3(cfg, key, key_len) kvconfig_contains_cstr((cfg), (Zstr)(key), (key_len)) /// /// Parse and fetch a boolean config value. @@ -267,12 +273,14 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, Bool /// -#define KvConfigGetBool(cfg, key, value) \ +#define KvConfigGetBool(...) OVERLOAD(KvConfigGetBool, __VA_ARGS__) +#define KvConfigGetBool_3(cfg, key, value) \ _Generic((key), Str *: kvconfig_get_bool_str, Zstr: kvconfig_get_bool_zstr, char *: kvconfig_get_bool_zstr)( \ (cfg), \ (key), \ (value) \ ) +#define KvConfigGetBool_4(cfg, key, key_len, value) kvconfig_get_bool_cstr((cfg), (Zstr)(key), (key_len), (value)) /// /// Parse and fetch a signed 64-bit integer config value. @@ -286,12 +294,14 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, I64 /// -#define KvConfigGetI64(cfg, key, value) \ +#define KvConfigGetI64(...) OVERLOAD(KvConfigGetI64, __VA_ARGS__) +#define KvConfigGetI64_3(cfg, key, value) \ _Generic((key), Str *: kvconfig_get_i64_str, Zstr: kvconfig_get_i64_zstr, char *: kvconfig_get_i64_zstr)( \ (cfg), \ (key), \ (value) \ ) +#define KvConfigGetI64_4(cfg, key, key_len, value) kvconfig_get_i64_cstr((cfg), (Zstr)(key), (key_len), (value)) /// /// Parse and fetch a double-precision floating config value. @@ -305,12 +315,14 @@ StrIter KvConfigParse(StrIter si, KvConfig *cfg); /// /// TAGS: KvConfig, Get, F64 /// -#define KvConfigGetF64(cfg, key, value) \ +#define KvConfigGetF64(...) OVERLOAD(KvConfigGetF64, __VA_ARGS__) +#define KvConfigGetF64_3(cfg, key, value) \ _Generic((key), Str *: kvconfig_get_f64_str, Zstr: kvconfig_get_f64_zstr, char *: kvconfig_get_f64_zstr)( \ (cfg), \ (key), \ (value) \ ) +#define KvConfigGetF64_4(cfg, key, key_len, value) kvconfig_get_f64_cstr((cfg), (Zstr)(key), (key_len), (value)) // Snake_case backends live in , pulled // in from the top of this file so the _Generic dispatch arms above can diff --git a/Include/Misra/Parsers/KvConfig/Private.h b/Include/Misra/Parsers/KvConfig/Private.h index d0544cd..1d6e628 100644 --- a/Include/Misra/Parsers/KvConfig/Private.h +++ b/Include/Misra/Parsers/KvConfig/Private.h @@ -17,21 +17,27 @@ extern "C" { #endif -// `KvConfig` is `typedef Map(Str, Str) KvConfig` -- defined in the -// public header before that header pulls this one in. + // `KvConfig` is `typedef Map(Str, Str) KvConfig` -- defined in the + // public header before that header pulls this one in. -Str kvconfig_get_zstr(KvConfig *cfg, Zstr key); -Str kvconfig_get_str(KvConfig *cfg, const Str *key); -Str *kvconfig_get_ptr_zstr(KvConfig *cfg, Zstr key); -Str *kvconfig_get_ptr_str(KvConfig *cfg, const Str *key); -bool kvconfig_contains_zstr(KvConfig *cfg, Zstr key); -bool kvconfig_contains_str(KvConfig *cfg, const Str *key); -bool kvconfig_get_bool_zstr(KvConfig *cfg, Zstr key, bool *value); -bool kvconfig_get_bool_str(KvConfig *cfg, const Str *key, bool *value); -bool kvconfig_get_i64_zstr(KvConfig *cfg, Zstr key, i64 *value); -bool kvconfig_get_i64_str(KvConfig *cfg, const Str *key, i64 *value); -bool kvconfig_get_f64_zstr(KvConfig *cfg, Zstr key, f64 *value); -bool kvconfig_get_f64_str(KvConfig *cfg, const Str *key, f64 *value); + Str kvconfig_get_zstr(KvConfig *cfg, Zstr key); + Str kvconfig_get_str(KvConfig *cfg, const Str *key); + Str kvconfig_get_cstr(KvConfig *cfg, Zstr key, size len); + Str *kvconfig_get_ptr_zstr(KvConfig *cfg, Zstr key); + Str *kvconfig_get_ptr_str(KvConfig *cfg, const Str *key); + Str *kvconfig_get_ptr_cstr(KvConfig *cfg, Zstr key, size len); + bool kvconfig_contains_zstr(KvConfig *cfg, Zstr key); + bool kvconfig_contains_str(KvConfig *cfg, const Str *key); + bool kvconfig_contains_cstr(KvConfig *cfg, Zstr key, size len); + bool kvconfig_get_bool_zstr(KvConfig *cfg, Zstr key, bool *value); + bool kvconfig_get_bool_str(KvConfig *cfg, const Str *key, bool *value); + bool kvconfig_get_bool_cstr(KvConfig *cfg, Zstr key, size len, bool *value); + bool kvconfig_get_i64_zstr(KvConfig *cfg, Zstr key, i64 *value); + bool kvconfig_get_i64_str(KvConfig *cfg, const Str *key, i64 *value); + bool kvconfig_get_i64_cstr(KvConfig *cfg, Zstr key, size len, i64 *value); + bool kvconfig_get_f64_zstr(KvConfig *cfg, Zstr key, f64 *value); + bool kvconfig_get_f64_str(KvConfig *cfg, const Str *key, f64 *value); + bool kvconfig_get_f64_cstr(KvConfig *cfg, Zstr key, size len, f64 *value); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/MachO.h b/Include/Misra/Parsers/MachO.h index 332b42f..2fc8398 100644 --- a/Include/Misra/Parsers/MachO.h +++ b/Include/Misra/Parsers/MachO.h @@ -146,6 +146,19 @@ typedef struct Macho { /// /// Open and parse a Mach-O file from disk. /// +/// Four call shapes on `path`: +/// `MachoOpen(out, path)` -- `path` is `Str *` / `Zstr` / +/// `char *`; default allocator. +/// `MachoOpen(out, path, alloc)` -- same, explicit allocator. +/// `MachoOpen(out, path, len, alloc)` -- `path` is a fixed-length view +/// (`Zstr`, `size`); explicit +/// allocator. Arity 3 is already +/// the NUL-terminated + allocator +/// form, so the fixed-length form +/// takes its allocator explicitly. +/// The fixed-length path is copied into a NUL-terminated stack buffer +/// (bounded by the platform path cap) before it reaches `open`. +/// /// SUCCESS : Returns true; parser owns the read-in buffer. /// FAILURE : Returns false on read / magic / load-command parse error. /// Fat/universal headers (`CAFEBABE`) are rejected as @@ -168,6 +181,7 @@ typedef struct Macho { Zstr: macho_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: macho_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define MachoOpen_4(out, path, len, alloc) macho_open_n((out), (Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Parse a Mach-O image from an in-memory byte range -- **L-value / @@ -226,18 +240,28 @@ void MachoDeinit(Macho *self); /// /// Find a section by (segment, section) name pair. /// +/// Two arities on the name keys: +/// `MachoFindSection(self, segment, section)` +/// -- each key is `Str *` / `Zstr` / `char *`. +/// `MachoFindSection(self, segment, segment_len, section, section_len)` +/// -- each key is a fixed-length view (`Zstr`, `size`). The bounds +/// are threaded into the compare (exactly N bytes, no copy). +/// /// SUCCESS : Returns a pointer to the matching `MachoSection`, /// borrowed from `self` (valid until `MachoDeinit`). /// FAILURE : Returns NULL when no section matches. /// /// TAGS: Parser, MachO, Section, Query /// -#define MachoFindSection(self, segment, section) \ +#define MachoFindSection(...) OVERLOAD(MachoFindSection, __VA_ARGS__) +#define MachoFindSection_3(self, segment, section) \ macho_find_section( \ (self), \ _Generic((segment), Str *: (Zstr)StrBegin((Str *)(segment)), Zstr: (Zstr)(segment), char *: (Zstr)(segment)), \ _Generic((section), Str *: (Zstr)StrBegin((Str *)(section)), Zstr: (Zstr)(section), char *: (Zstr)(section)) \ ) +#define MachoFindSection_5(self, segment, segment_len, section, section_len) \ + macho_find_section_cstr((self), (Zstr)(segment), (segment_len), (Zstr)(section), (section_len)) /// /// Look up the symbol whose `value` is closest-not-greater than diff --git a/Include/Misra/Parsers/MachO/Private.h b/Include/Misra/Parsers/MachO/Private.h index 98d9f8e..9d6a9bc 100644 --- a/Include/Misra/Parsers/MachO/Private.h +++ b/Include/Misra/Parsers/MachO/Private.h @@ -17,12 +17,15 @@ extern "C" { #endif -typedef struct Macho Macho; -typedef struct MachoSection MachoSection; + typedef struct Macho Macho; + typedef struct MachoSection MachoSection; -bool macho_open(Macho *out, Zstr path, Allocator *alloc); -bool macho_open_from_memory_copy(Macho *out, const u8 *data, size data_size, Allocator *alloc); -const MachoSection *macho_find_section(const Macho *self, Zstr segment, Zstr section); + bool macho_open(Macho *out, Zstr path, Allocator *alloc); + bool macho_open_n(Macho *out, Zstr path, size len, Allocator *alloc); + bool macho_open_from_memory_copy(Macho *out, const u8 *data, size data_size, Allocator *alloc); + const MachoSection *macho_find_section(const Macho *self, Zstr segment, Zstr section); + const MachoSection * + macho_find_section_cstr(const Macho *self, Zstr segment, size segment_len, Zstr section, size section_len); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/Pdb.h b/Include/Misra/Parsers/Pdb.h index 5bb655a..833914c 100644 --- a/Include/Misra/Parsers/Pdb.h +++ b/Include/Misra/Parsers/Pdb.h @@ -122,6 +122,15 @@ typedef struct Pdb { /// /// Open and parse a PDB from disk. /// +/// Call shapes via `OVERLOAD` + `_Generic` on `path`: +/// `PdbOpen(out, path)` -- `path` is `Str *` / `Zstr`; +/// reads through `MisraScope`. +/// `PdbOpen(out, path, alloc)` -- `path` is `Str *` / `Zstr`; +/// reads through `alloc`. +/// `PdbOpen(out, path, len, alloc)` -- `path` is a fixed-length view +/// (`Zstr`, `size`); copied into a +/// stack buffer for the syscall. +/// /// SUCCESS : Returns true; parser owns the read-in buffer. /// FAILURE : Returns false; logs the failing step. `out` is left zeroed. /// @@ -142,6 +151,7 @@ typedef struct Pdb { Zstr: pdb_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: pdb_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define PdbOpen_4(out, path, len, alloc) pdb_open_n((out), (Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Open and parse a PDB from an in-memory byte range -- **L-value / diff --git a/Include/Misra/Parsers/Pdb/Private.h b/Include/Misra/Parsers/Pdb/Private.h index f0b3812..4751154 100644 --- a/Include/Misra/Parsers/Pdb/Private.h +++ b/Include/Misra/Parsers/Pdb/Private.h @@ -17,10 +17,11 @@ extern "C" { #endif -typedef struct Pdb Pdb; + typedef struct Pdb Pdb; -bool pdb_open(Pdb *out, Zstr path, Allocator *alloc); -bool pdb_open_from_memory_copy(Pdb *out, const u8 *data, size data_size, Allocator *alloc); + bool pdb_open(Pdb *out, Zstr path, Allocator *alloc); + bool pdb_open_n(Pdb *out, Zstr path, size len, Allocator *alloc); + bool pdb_open_from_memory_copy(Pdb *out, const u8 *data, size data_size, Allocator *alloc); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/Pe.h b/Include/Misra/Parsers/Pe.h index 2351abc..e674312 100644 --- a/Include/Misra/Parsers/Pe.h +++ b/Include/Misra/Parsers/Pe.h @@ -122,10 +122,19 @@ typedef struct Pe { /// /// Open and parse a PE file from disk. /// -/// out[out] : Populated on success. -/// path[in] : Filesystem path. `Str *` preferred; `Zstr ` accepted. -/// alloc[in] : Allocator for the read-in buffer and the sections -/// vector. Must outlive the `Pe`. +/// Call shapes via `OVERLOAD` + `_Generic` on `path`: +/// `PeOpen(out, path)` -- `path` is `Str *` or `Zstr`. +/// `PeOpen(out, path, alloc)` -- same, explicit allocator. +/// `PeOpen(out, path, path_len, alloc)`-- `path` is a fixed-length view +/// (`Zstr`, `size`); it is copied +/// into a stack buffer and +/// NUL-terminated for the open. +/// +/// out[out] : Populated on success. +/// path[in] : Filesystem path. `Str *` preferred; `Zstr ` accepted. +/// path_len[in] : Length of `path` for the fixed-length form. +/// alloc[in] : Allocator for the read-in buffer and the sections +/// vector. Must outlive the `Pe`. /// /// SUCCESS : Returns true; `out` owns the read-in buffer. /// FAILURE : Returns false; logs the failing step. `out` is left zeroed. @@ -147,6 +156,7 @@ typedef struct Pe { Zstr: pe_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: pe_open((out), (Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define PeOpen_4(out, path, len, alloc) pe_open_n((out), (Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Parse a PE image from an in-memory byte range -- **L-value / @@ -204,17 +214,26 @@ void PeDeinit(Pe *self); /// Find a section by name (first match; PE allows duplicates but /// they're vanishingly rare). /// +/// Call shapes via `OVERLOAD` + `_Generic` on `name`: +/// `PeFindSection(self, name)` -- `name` is `Str *` or `Zstr`. +/// `PeFindSection(self, name, name_len)` -- `name` is a fixed-length +/// view (`Zstr`, `size`); +/// matched over exactly +/// `name_len` bytes, no copy. +/// /// SUCCESS : Returns a pointer to the matching `PeSection`, borrowed /// from `self` (valid until `PeDeinit`). /// FAILURE : Returns NULL when no section matches. /// /// TAGS: Parser, PE, Section, Query /// -#define PeFindSection(self, name) \ +#define PeFindSection(...) OVERLOAD(PeFindSection, __VA_ARGS__) +#define PeFindSection_2(self, name) \ _Generic((name), Str *: pe_find_section_str, Zstr: pe_find_section_zstr, char *: pe_find_section_zstr)( \ (self), \ (name) \ ) +#define PeFindSection_3(self, name, name_len) pe_find_section_cstr((self), (Zstr)(name), (name_len)) /// /// Convert an RVA (offset from `ImageBase`) to a file offset by diff --git a/Include/Misra/Parsers/Pe/Private.h b/Include/Misra/Parsers/Pe/Private.h index fa04690..90dd07f 100644 --- a/Include/Misra/Parsers/Pe/Private.h +++ b/Include/Misra/Parsers/Pe/Private.h @@ -18,13 +18,15 @@ extern "C" { #endif -typedef struct Pe Pe; -typedef struct PeSection PeSection; + typedef struct Pe Pe; + typedef struct PeSection PeSection; -bool pe_open(Pe *out, Zstr path, Allocator *alloc); -bool pe_open_from_memory_copy(Pe *out, const u8 *data, size data_size, Allocator *alloc); -const PeSection *pe_find_section_zstr(const Pe *self, Zstr name); -const PeSection *pe_find_section_str(const Pe *self, const Str *name); + bool pe_open(Pe *out, Zstr path, Allocator *alloc); + bool pe_open_n(Pe *out, Zstr path, size len, Allocator *alloc); + bool pe_open_from_memory_copy(Pe *out, const u8 *data, size data_size, Allocator *alloc); + const PeSection *pe_find_section_zstr(const Pe *self, Zstr name); + const PeSection *pe_find_section_str(const Pe *self, const Str *name); + const PeSection *pe_find_section_cstr(const Pe *self, Zstr name, size name_len); #ifdef __cplusplus } diff --git a/Include/Misra/Parsers/ProcMaps.h b/Include/Misra/Parsers/ProcMaps.h index d2947b0..f02694d 100644 --- a/Include/Misra/Parsers/ProcMaps.h +++ b/Include/Misra/Parsers/ProcMaps.h @@ -32,33 +32,31 @@ typedef enum ProcMapPerms { } ProcMapPerms; /// -/// One line of `/proc/self/maps`. `path` is borrowed from the -/// `ProcMaps.raw` buffer and stays valid until `ProcMapsDeinit`. May -/// be empty for anonymous mappings (heap, stacks, vdso, etc.). +/// One line of `/proc/self/maps`. `path` is an owned, NUL-terminated copy of the +/// mapping's backing file, freed by `ProcMapsDeinit`. It is an empty `Str` +/// (`StrLen(&path) == 0`) for anonymous mappings (heap, stacks, vdso, etc.). /// typedef struct ProcMapEntry { - u64 start; // runtime virtual address (inclusive) - u64 end; // runtime virtual address (exclusive) - u32 perms; // bitmask of ProcMapPerms - u64 file_offset; // offset within the backing file - Zstr path; // backing file path, or "" if anonymous + u64 start; // runtime virtual address (inclusive) + u64 end; // runtime virtual address (exclusive) + u32 perms; // bitmask of ProcMapPerms + u64 file_offset; // offset within the backing file + Str path; // owned copy of the mapping path (empty for anonymous mappings) } ProcMapEntry; typedef Vec(ProcMapEntry) ProcMapEntries; typedef struct ProcMaps { - Str raw; // owns the raw /proc/self/maps bytes - ProcMapEntries entries; // pointers into `raw` + ProcMapEntries entries; // each entry owns its path copy u64 min_addr; // lowest `start` across all entries (0 if none) } ProcMaps; /// -/// Read and parse `/proc/self/maps`. The full file is held inside -/// `out->raw` for the lifetime of the ProcMaps so each entry's `path` -/// can borrow from it without a separate copy. +/// Read and parse `/proc/self/maps`. The file is parsed into `out->entries`, +/// each entry owning a copy of its path; the raw buffer is not retained. /// /// out[out] : Populated on success. -/// alloc[in] : Allocator for the raw buffer and entries vector. +/// alloc[in] : Allocator for the entries vector and each entry's path copy. /// /// SUCCESS : Returns true; `out->entries` is populated. /// FAILURE : Returns false; logs the failing step. `out` is left zeroed. diff --git a/Include/Misra/Std/Container/Buf.h b/Include/Misra/Std/Container/Buf.h index 1326452..93cc17e 100644 --- a/Include/Misra/Std/Container/Buf.h +++ b/Include/Misra/Std/Container/Buf.h @@ -59,7 +59,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Clear, Reuse /// -#define BufClear(b) VecClear(b) +#define BufClear(b) VecClear(b) // Read-only accessors. The leading `((void)0, ...)` makes each macro a // comma expression, and the result of a C comma expression is not an @@ -73,7 +73,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Length, Accessor /// -#define BufLength(b) ((void)0, (b)->length) +#define BufLength(b) ((void)0, (b)->length) /// /// Pointer to the contiguous byte storage backing `b`. Read-only at @@ -83,7 +83,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Data, Accessor /// -#define BufData(b) ((void)0, (b)->data) +#define BufData(b) ((void)0, (b)->data) /// /// Allocator backing `b`'s storage. Read-only; rebinding the @@ -91,7 +91,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Allocator, Accessor /// -#define BufAllocator(b) ((void)0, (b)->allocator) +#define BufAllocator(b) ((void)0, (b)->allocator) /// /// Ensure `b` has capacity for at least `n` bytes without changing @@ -118,7 +118,7 @@ typedef Iter(const u8) BufIter; /// /// TAGS: Buf, Resize, Capacity, Allocation /// -#define BufResize(b, n) VecResize((b), (n)) +#define BufResize(b, n) VecResize((b), (n)) /// /// Construct a `BufIter` over `[data, data + length)`. The iterator @@ -178,6 +178,9 @@ static inline bool BufPushBytes(Buf *b, const u8 *data, size n) { /// /// Read a single byte at the cursor and advance one byte. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the byte at `c->pos` and /// `c->pos` has advanced by one. /// FAILURE : Returns `false` when fewer than one byte remains in `c` @@ -197,6 +200,9 @@ static inline bool BufReadU8(BufIter *c, u8 *out) { /// /// Read two little-endian bytes (low byte first) and advance two bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u16` and /// `c->pos` has advanced by two. /// FAILURE : Returns `false` when fewer than two bytes remain in `c`; @@ -216,6 +222,9 @@ static inline bool BufReadU16LE(BufIter *c, u16 *out) { /// /// Read two big-endian bytes (high byte first) and advance two bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u16` and /// `c->pos` has advanced by two. /// FAILURE : Returns `false` when fewer than two bytes remain in `c`; @@ -235,6 +244,9 @@ static inline bool BufReadU16BE(BufIter *c, u16 *out) { /// /// Read four little-endian bytes and advance four bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u32` and /// `c->pos` has advanced by four. /// FAILURE : Returns `false` when fewer than four bytes remain in `c`; @@ -255,6 +267,9 @@ static inline bool BufReadU32LE(BufIter *c, u32 *out) { /// /// Read four big-endian bytes and advance four bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u32` and /// `c->pos` has advanced by four. /// FAILURE : Returns `false` when fewer than four bytes remain in `c`; @@ -275,6 +290,9 @@ static inline bool BufReadU32BE(BufIter *c, u32 *out) { /// /// Read eight little-endian bytes and advance eight bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u64` and /// `c->pos` has advanced by eight. /// FAILURE : Returns `false` when fewer than eight bytes remain in `c`; @@ -298,6 +316,9 @@ static inline bool BufReadU64LE(BufIter *c, u64 *out) { /// /// Read eight big-endian bytes and advance eight bytes. /// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded value. +/// /// SUCCESS : Returns `true`; `*out` holds the decoded `u64` and /// `c->pos` has advanced by eight. /// FAILURE : Returns `false` when fewer than eight bytes remain in `c`; @@ -318,6 +339,168 @@ static inline bool BufReadU64BE(BufIter *c, u64 *out) { return true; } +/// +/// Read a single byte at the cursor as a two's-complement signed value and +/// advance one byte. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the signed byte at `c->pos` and +/// `c->pos` has advanced by one. +/// FAILURE : Returns `false` when fewer than one byte remains in `c` +/// (`c->pos >= c->length`); `*out` is unchanged and `c->pos` +/// is unchanged. +/// +/// TAGS: Buf, Read, I8 +/// +static inline bool BufReadI8(BufIter *c, i8 *out) { + u8 raw = 0; + if (!BufReadU8(c, &raw)) { + return false; + } + *out = (i8)raw; + return true; +} + +/// +/// Read two little-endian bytes (low byte first) as a two's-complement signed +/// value and advance two bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i16` and +/// `c->pos` has advanced by two. +/// FAILURE : Returns `false` when fewer than two bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I16, LittleEndian +/// +static inline bool BufReadI16LE(BufIter *c, i16 *out) { + u16 raw = 0; + if (!BufReadU16LE(c, &raw)) { + return false; + } + *out = (i16)raw; + return true; +} + +/// +/// Read two big-endian bytes (high byte first) as a two's-complement signed +/// value and advance two bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i16` and +/// `c->pos` has advanced by two. +/// FAILURE : Returns `false` when fewer than two bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I16, BigEndian +/// +static inline bool BufReadI16BE(BufIter *c, i16 *out) { + u16 raw = 0; + if (!BufReadU16BE(c, &raw)) { + return false; + } + *out = (i16)raw; + return true; +} + +/// +/// Read four little-endian bytes as a two's-complement signed value and advance +/// four bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i32` and +/// `c->pos` has advanced by four. +/// FAILURE : Returns `false` when fewer than four bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I32, LittleEndian +/// +static inline bool BufReadI32LE(BufIter *c, i32 *out) { + u32 raw = 0; + if (!BufReadU32LE(c, &raw)) { + return false; + } + *out = (i32)raw; + return true; +} + +/// +/// Read four big-endian bytes as a two's-complement signed value and advance +/// four bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i32` and +/// `c->pos` has advanced by four. +/// FAILURE : Returns `false` when fewer than four bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I32, BigEndian +/// +static inline bool BufReadI32BE(BufIter *c, i32 *out) { + u32 raw = 0; + if (!BufReadU32BE(c, &raw)) { + return false; + } + *out = (i32)raw; + return true; +} + +/// +/// Read eight little-endian bytes as a two's-complement signed value and advance +/// eight bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i64` and +/// `c->pos` has advanced by eight. +/// FAILURE : Returns `false` when fewer than eight bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I64, LittleEndian +/// +static inline bool BufReadI64LE(BufIter *c, i64 *out) { + u64 raw = 0; + if (!BufReadU64LE(c, &raw)) { + return false; + } + *out = (i64)raw; + return true; +} + +/// +/// Read eight big-endian bytes as a two's-complement signed value and advance +/// eight bytes. +/// +/// c[in,out] : Byte cursor; read from and advanced past the field on success. +/// out[out] : Receives the decoded signed value. +/// +/// SUCCESS : Returns `true`; `*out` holds the decoded `i64` and +/// `c->pos` has advanced by eight. +/// FAILURE : Returns `false` when fewer than eight bytes remain in `c`; +/// `*out` is unchanged and `c->pos` is unchanged. +/// +/// TAGS: Buf, Read, I64, BigEndian +/// +static inline bool BufReadI64BE(BufIter *c, i64 *out) { + u64 raw = 0; + if (!BufReadU64BE(c, &raw)) { + return false; + } + *out = (i64)raw; + return true; +} + /// LEB128 unsigned. Fails on truncation or width overflow. /// /// SUCCESS : Returns `true`; `*out` holds the decoded value and `c->pos` diff --git a/Include/Misra/Std/Container/List/Insert.h b/Include/Misra/Std/Container/List/Insert.h index d36d63d..d29f6eb 100644 --- a/Include/Misra/Std/Container/List/Insert.h +++ b/Include/Misra/Std/Container/List/Insert.h @@ -33,7 +33,7 @@ #define ListInsertL(l, lval, idx) \ (ValidateList(l), \ CHECK_TYPE_EQUIVALENCE(TYPE_OF(lval), LIST_DATA_TYPE(l)), \ - list_insert_one_l(GENERIC_LIST(l), &LVAL_AS(LIST_DATA_TYPE(l), lval), &(lval), sizeof(LIST_DATA_TYPE(l)), (idx))) + list_insert_one_l(GENERIC_LIST(l), &(lval), sizeof(LIST_DATA_TYPE(l)), (idx))) /// /// Insert a single element at the given index. R-value form: source is treated diff --git a/Include/Misra/Std/Container/List/Private.h b/Include/Misra/Std/Container/List/Private.h index f56839e..b48196d 100644 --- a/Include/Misra/Std/Container/List/Private.h +++ b/Include/Misra/Std/Container/List/Private.h @@ -27,7 +27,7 @@ GenericListNode *get_node_relative_to_list_node(GenericListNode *node, i64 ridx) GenericListNode *get_node_random_access(GenericList *list, GenericListNode *node, u64 nidx, i64 ridx); GenericListNode *get_node_for_list_iteration(GenericList *list, GenericListNode *node, u64 nidx, u64 target_idx); -bool list_insert_one_l(GenericList *list, const void *item_copy, void *source, u64 item_size, u64 idx); +bool list_insert_one_l(GenericList *list, void *source, u64 item_size, u64 idx); bool list_insert_one_r(GenericList *list, const void *item_copy, u64 item_size, u64 idx); bool list_insert_range_l(GenericList *list, void *items, u64 item_size, u64 count); bool list_insert_range_r(GenericList *list, const void *items, u64 item_size, u64 count); diff --git a/Include/Misra/Std/Container/Vec/Foreach.h b/Include/Misra/Std/Container/Vec/Foreach.h index 3bef967..ca82ed2 100644 --- a/Include/Misra/Std/Container/Vec/Foreach.h +++ b/Include/Misra/Std/Container/Vec/Foreach.h @@ -54,9 +54,9 @@ #define VecForeachReverseIdx(v, var, idx) \ for (TYPE_OF(v) UNPL(pv) = (v); UNPL(pv); UNPL(pv) = NULL) \ if ((ValidateVec(UNPL(pv)), 1) && UNPL(pv)->length > 0) \ - for (u64 idx = UNPL(pv)->length; idx-- > 0 && idx < UNPL(pv)->length;) \ - for (u8 UNPL(run_once) = 1; UNPL(run_once); UNPL(run_once) = 0) \ - for (VEC_DATATYPE(UNPL(pv)) var = VecAt(UNPL(pv), idx); UNPL(run_once); UNPL(run_once) = 0) + for (u64 idx = UNPL(pv)->length, UNPL(d) = 1; UNPL(d); UNPL(d)--) \ + for (VEC_DATATYPE(UNPL(pv)) var = {0}; \ + idx-- > 0 && idx < UNPL(pv)->length && (var = VecAt(UNPL(pv), idx), 1);) /// /// Iterate over each element `var` of given vector `v` at each index `idx` into the vector. @@ -107,9 +107,9 @@ #define VecForeachPtrReverseIdx(v, var, idx) \ for (TYPE_OF(v) UNPL(pv) = (v); UNPL(pv); UNPL(pv) = NULL) \ if ((ValidateVec(UNPL(pv)), 1) && UNPL(pv)->length > 0) \ - for (u64 idx = UNPL(pv)->length; idx-- > 0 && idx < UNPL(pv)->length;) \ - for (u8 UNPL(run_once) = 1; UNPL(run_once); UNPL(run_once) = 0) \ - for (VEC_DATATYPE(UNPL(pv)) *var = VecPtrAt(UNPL(pv), idx); UNPL(run_once); UNPL(run_once) = 0) + for (u64 idx = UNPL(pv)->length, UNPL(d) = 1; UNPL(d); UNPL(d)--) \ + for (VEC_DATATYPE(UNPL(pv)) *var = NULL; \ + idx-- > 0 && idx < UNPL(pv)->length && (var = VecPtrAt(UNPL(pv), idx), 1);) /// /// Walk each element of `v` forward, binding `var` to the element value. diff --git a/Include/Misra/Std/Container/Vec/Insert.h b/Include/Misra/Std/Container/Vec/Insert.h index c39c5b0..d9b656e 100644 --- a/Include/Misra/Std/Container/Vec/Insert.h +++ b/Include/Misra/Std/Container/Vec/Insert.h @@ -39,7 +39,7 @@ #define VecInsertL(v, lval, idx) \ (ValidateVec(v), \ CHECK_TYPE_EQUIVALENCE(TYPE_OF(lval), VEC_DATATYPE(v)), \ - vec_insert_one_l(GENERIC_VEC(v), &LVAL_AS(VEC_DATATYPE(v), lval), &(lval), sizeof(VEC_DATATYPE(v)), (idx), true)) + vec_insert_one_l(GENERIC_VEC(v), &(lval), sizeof(VEC_DATATYPE(v)), (idx), true)) /// /// Insert a single element at the given index, preserving order of trailing @@ -101,14 +101,7 @@ #define VecInsertFastL(v, lval, idx) \ (ValidateVec(v), \ CHECK_TYPE_EQUIVALENCE(TYPE_OF(lval), VEC_DATATYPE(v)), \ - vec_insert_one_l( \ - GENERIC_VEC(v), \ - &LVAL_AS(VEC_DATATYPE(v), lval), \ - &(lval), \ - sizeof(VEC_DATATYPE(v)), \ - (idx), \ - false \ - )) + vec_insert_one_l(GENERIC_VEC(v), &(lval), sizeof(VEC_DATATYPE(v)), (idx), false)) /// /// Insert a single element using fast (order-not-preserving) placement. diff --git a/Include/Misra/Std/Container/Vec/Private.h b/Include/Misra/Std/Container/Vec/Private.h index 8a36001..7413cd7 100644 --- a/Include/Misra/Std/Container/Vec/Private.h +++ b/Include/Misra/Std/Container/Vec/Private.h @@ -31,14 +31,7 @@ extern "C" { size find_idx_vec(GenericVec *vec, const void *item_data, size item_size, GenericCompare comp); void validate_vec(const GenericVec *v); - bool vec_insert_one_l( - GenericVec *vec, - const void *item_copy, - void *source, - size item_size, - size idx, - bool preserve_order - ); + bool vec_insert_one_l(GenericVec *vec, void *source, size item_size, size idx, bool preserve_order); bool vec_insert_one_r(GenericVec *vec, const void *item_copy, size item_size, size idx, bool preserve_order); bool vec_insert_range_l(GenericVec *vec, void *items, size item_size, size idx, size count, bool preserve_order); bool vec_insert_range_r( diff --git a/Include/Misra/Std/File.h b/Include/Misra/Std/File.h index 57c99e4..1dca94c 100644 --- a/Include/Misra/Std/File.h +++ b/Include/Misra/Std/File.h @@ -66,9 +66,17 @@ typedef enum FileWhence { /// the `"b"` suffix is accepted but has no effect on the /// implementation. /// -/// path[in] : Path to open. Prefer `Str *` (carries length, can't -/// silently drop the NUL terminator). `Zstr` is accepted -/// as a literal / borrowed-buffer convenience. +/// Three call shapes via `OVERLOAD` + `_Generic` on `path`: +/// `FileOpen(path, mode)` -- `path` is `Str *` or `Zstr`. +/// `FileOpen(path, path_len, mode)` -- `path` is a fixed-length view +/// (`Zstr`, `size`); it is copied +/// into a stack buffer and +/// NUL-terminated for the syscall. +/// +/// path[in] : Path to open. Prefer `Str *` (carries length, can't +/// silently drop the NUL terminator). `Zstr` is accepted +/// as a literal / borrowed-buffer convenience. +/// path_len[in] : Length of `path` for the 3-arg fixed-length form. /// /// SUCCESS : Returns a File where `FileIsOpen(&out)` is true. /// FAILURE : Returns a File where `FileIsOpen(&out)` is false. @@ -76,13 +84,16 @@ typedef enum FileWhence { /// TAGS: File, Open, API /// File file_open(Zstr path, Zstr mode); -#define FileOpen(path, mode) \ +File file_open_n(Zstr path, size len, Zstr mode); +#define FileOpen(...) OVERLOAD(FileOpen, __VA_ARGS__) +#define FileOpen_2(path, mode) \ _Generic( \ (path), \ Str *: file_open((Zstr)StrBegin((Str *)(path)), (mode)), \ Zstr: file_open((Zstr)(path), (mode)), \ char *: file_open((Zstr)(path), (mode)) \ ) +#define FileOpen_3(path, len, mode) file_open_n((Zstr)(path), (len), (mode)) /// /// Borrow a POSIX fd into a `File`. The returned File has `owns = false` @@ -199,7 +210,10 @@ i64 file_read_to_buf(File *f, Buf *out); /// i64 file_read_and_close_to_buf(Zstr path, Buf *out); i64 file_read_and_close_to_str(Zstr path, Str *out); -#define FileReadAndClose(path, out) \ +i64 file_read_and_close_to_buf_n(Zstr path, size len, Buf *out); +i64 file_read_and_close_to_str_n(Zstr path, size len, Str *out); +#define FileReadAndClose(...) OVERLOAD(FileReadAndClose, __VA_ARGS__) +#define FileReadAndClose_2(path, out) \ _Generic( \ (out), \ Buf *: _Generic( \ @@ -215,6 +229,12 @@ i64 file_read_and_close_to_str(Zstr path, Str *out); char *: file_read_and_close_to_str((Zstr)(path), (Str *)(out)) \ ) \ ) +#define FileReadAndClose_3(path, len, out) \ + _Generic( \ + (out), \ + Buf *: file_read_and_close_to_buf_n((Zstr)(path), (len), (Buf *)(out)), \ + Str *: file_read_and_close_to_str_n((Zstr)(path), (len), (Str *)(out)) \ + ) // FileGetSize lives in `Sys/Dir.h` -- path-based size query that // goes straight to the kernel (open + lseek(SEEK_END) + close, or diff --git a/Include/Misra/Sys/Dir.h b/Include/Misra/Sys/Dir.h index 5dca385..a73cd10 100644 --- a/Include/Misra/Sys/Dir.h +++ b/Include/Misra/Sys/Dir.h @@ -112,6 +112,7 @@ typedef Vec(DirEntry) DirContents; /// TAGS: System, FileSystem, Directory /// DirContents dir_get_contents(Zstr path, Allocator *alloc); +DirContents dir_get_contents_cstr(Zstr path, size len, Allocator *alloc); #define DirGetContents(...) OVERLOAD(DirGetContents, __VA_ARGS__) #define DirGetContents_1(path) \ _Generic( \ @@ -127,6 +128,7 @@ DirContents dir_get_contents(Zstr path, Allocator *alloc); Zstr: dir_get_contents((Zstr)(path), ALLOCATOR_OF(alloc)), \ char *: dir_get_contents((Zstr)(path), ALLOCATOR_OF(alloc)) \ ) +#define DirGetContents_3(path, len, alloc) dir_get_contents_cstr((Zstr)(path), (len), ALLOCATOR_OF(alloc)) /// /// Get size of file without opening it. @@ -139,13 +141,16 @@ DirContents dir_get_contents(Zstr path, Allocator *alloc); /// TAGS: System, File, Metadata /// i64 file_get_size(Zstr filename); -#define FileGetSize(path) \ +i64 file_get_size_cstr(Zstr filename, size len); +#define FileGetSize(...) OVERLOAD(FileGetSize, __VA_ARGS__) +#define FileGetSize_1(path) \ _Generic( \ (path), \ Str *: file_get_size((Zstr)StrBegin((Str *)(path))), \ Zstr: file_get_size((Zstr)(path)), \ char *: file_get_size((Zstr)(path)) \ ) +#define FileGetSize_2(path, len) file_get_size_cstr((Zstr)(path), (len)) /// /// Remove a regular file. Direct syscall (`unlink` on Linux x86_64 / @@ -167,13 +172,16 @@ i64 file_get_size(Zstr filename); /// TAGS: System, File, FileSystem /// i8 file_remove(Zstr path); -#define FileRemove(path) \ +i8 file_remove_cstr(Zstr path, size len); +#define FileRemove(...) OVERLOAD(FileRemove, __VA_ARGS__) +#define FileRemove_1(path) \ _Generic( \ (path), \ Str *: file_remove((Zstr)StrBegin((Str *)(path))), \ Zstr: file_remove((Zstr)(path)), \ char *: file_remove((Zstr)(path)) \ ) +#define FileRemove_2(path, len) file_remove_cstr((Zstr)(path), (len)) /// /// Remove an empty directory. Direct syscall (`rmdir` on Linux @@ -190,13 +198,16 @@ i8 file_remove(Zstr path); /// TAGS: System, Directory, FileSystem /// i8 dir_remove(Zstr path); -#define DirRemove(path) \ +i8 dir_remove_cstr(Zstr path, size len); +#define DirRemove(...) OVERLOAD(DirRemove, __VA_ARGS__) +#define DirRemove_1(path) \ _Generic( \ (path), \ Str *: dir_remove((Zstr)StrBegin((Str *)(path))), \ Zstr: dir_remove((Zstr)(path)), \ char *: dir_remove((Zstr)(path)) \ ) +#define DirRemove_2(path, len) dir_remove_cstr((Zstr)(path), (len)) /// /// Create a single directory. Direct syscall (`mkdir` on Linux @@ -213,13 +224,16 @@ i8 dir_remove(Zstr path); /// TAGS: System, Directory, FileSystem /// i8 dir_create(Zstr path); -#define DirCreate(path) \ +i8 dir_create_cstr(Zstr path, size len); +#define DirCreate(...) OVERLOAD(DirCreate, __VA_ARGS__) +#define DirCreate_1(path) \ _Generic( \ (path), \ Str *: dir_create((Zstr)StrBegin((Str *)(path))), \ Zstr: dir_create((Zstr)(path)), \ char *: dir_create((Zstr)(path)) \ ) +#define DirCreate_2(path, len) dir_create_cstr((Zstr)(path), (len)) /// /// Recursive `mkdir -p`. Creates all missing path components. @@ -235,13 +249,16 @@ i8 dir_create(Zstr path); /// TAGS: System, Directory, FileSystem /// i8 dir_create_all(Zstr path); -#define DirCreateAll(path) \ +i8 dir_create_all_cstr(Zstr path, size len); +#define DirCreateAll(...) OVERLOAD(DirCreateAll, __VA_ARGS__) +#define DirCreateAll_1(path) \ _Generic( \ (path), \ Str *: dir_create_all((Zstr)StrBegin((Str *)(path))), \ Zstr: dir_create_all((Zstr)(path)), \ char *: dir_create_all((Zstr)(path)) \ ) +#define DirCreateAll_2(path, len) dir_create_all_cstr((Zstr)(path), (len)) /// /// Recursive `rm -rf`. Removes a directory tree (regular files, @@ -256,12 +273,15 @@ i8 dir_create_all(Zstr path); /// TAGS: System, Directory, FileSystem /// i8 dir_remove_all(Zstr path); -#define DirRemoveAll(path) \ +i8 dir_remove_all_cstr(Zstr path, size len); +#define DirRemoveAll(...) OVERLOAD(DirRemoveAll, __VA_ARGS__) +#define DirRemoveAll_1(path) \ _Generic( \ (path), \ Str *: dir_remove_all((Zstr)StrBegin((Str *)(path))), \ Zstr: dir_remove_all((Zstr)(path)), \ char *: dir_remove_all((Zstr)(path)) \ ) +#define DirRemoveAll_2(path, len) dir_remove_all_cstr((Zstr)(path), (len)) #endif // MISRA_SYS_DIR_H diff --git a/Include/Misra/Sys/Dns.h b/Include/Misra/Sys/Dns.h index ebcfc5a..f0ddccc 100644 --- a/Include/Misra/Sys/Dns.h +++ b/Include/Misra/Sys/Dns.h @@ -178,7 +178,9 @@ extern "C" { /// strategy -- we always look up both A and AAAA). /// /// hostname[in] : Hostname to resolve. Prefer `Str *`; `Zstr` - /// accepted. Trailing dot tolerated. + /// accepted; a `(Zstr, len)` counted view is + /// accepted via the 6-arg form. Trailing dot + /// tolerated. /// port[in] : Port number to stamp on every returned address. /// kind[in] : `SOCKET_KIND_TCP` or `SOCKET_KIND_UDP`. /// out[in,out] : Vec to append results to. Stays untouched on failure. @@ -192,6 +194,14 @@ extern "C" { /// bool dns_resolve_5_zstr(DnsResolver *self, Zstr hostname, u16 port, SocketKind kind, DnsAddrs *out); bool dns_resolve_5_str(DnsResolver *self, const Str *hostname, u16 port, SocketKind kind, DnsAddrs *out); + bool dns_resolve_6_cstr( + DnsResolver *self, + Zstr hostname, + u64 hostname_len, + u16 port, + SocketKind kind, + DnsAddrs *out + ); #define DnsResolve_5(self, hostname, port, kind, out) \ _Generic( \ (hostname), \ @@ -199,6 +209,8 @@ extern "C" { Zstr: dns_resolve_5_zstr((self), (Zstr)(hostname), (port), (kind), (out)), \ char *: dns_resolve_5_zstr((self), (Zstr)(hostname), (port), (kind), (out)) \ ) +#define DnsResolve_6(self, hostname, hostname_len, port, kind, out) \ + dns_resolve_6_cstr((self), (Zstr)(hostname), (hostname_len), (port), (kind), (out)) /// /// Spec-based overload (vec form). Accepts a single `"host:port"` @@ -243,7 +255,16 @@ extern "C" { /// `DnsResolve` dispatches by argument count. The 4-arg form /// additionally dispatches on the `out` parameter type: /// `DnsAddrs *` selects the vec form, `SocketAddr *` selects the - /// single-addr form. + /// single-addr form. The 6-arg form is the counted-hostname + /// overload: `(self, hostname, hostname_len, port, kind, out)` + /// takes a `(Zstr, len)` view (the fourth string-input branch, + /// alongside `Str *` / `Zstr` / `char *` in the 5-arg form). + /// + /// The 4-arg spec form has no counted `(Zstr, len)` sibling: its + /// base+1 arity is 5, which is already the hostname form, so a + /// counted spec cannot be routed through this by-count OVERLOAD + /// without ambiguity. Callers with a non-terminated `host:port` + /// slice should terminate it (or wrap in a `Str`) themselves. /// /// TAGS: Dns, Resolve, API /// diff --git a/Include/Misra/Sys/Socket.h b/Include/Misra/Sys/Socket.h index cb995ca..cc1739e 100644 --- a/Include/Misra/Sys/Socket.h +++ b/Include/Misra/Sys/Socket.h @@ -146,7 +146,7 @@ bool socket_addr_parse_zstr(SocketAddr *out, Zstr spec, SocketKind kind); /// `Str` overload of `SocketAddrParse`. Identical contract to the Zstr /// arm above; provided so callers holding a `Str` don't have to reach /// into `.data`. The `_Generic` macro below routes through this for -/// `Str *` / `const Str *` inputs. +/// `Str *` inputs. /// /// SUCCESS : Returns true; `*out` populated. /// FAILURE : Returns false; `*out` zeroed. Silent (no log) — caller is @@ -155,13 +155,31 @@ bool socket_addr_parse_zstr(SocketAddr *out, Zstr spec, SocketKind kind); /// TAGS: Socket, Parse, Address /// bool socket_addr_parse_str(SocketAddr *out, const Str *spec, SocketKind kind); -#define SocketAddrParse(out, spec, kind) \ - _Generic( \ - (spec), \ - Str *: socket_addr_parse_str((out), (const Str *)(spec), (kind)), \ - Zstr: socket_addr_parse_zstr((out), (Zstr)(spec), (kind)), \ - char *: socket_addr_parse_zstr((out), (Zstr)(spec), (kind)) \ + +/// +/// Fixed-length (`Zstr`, `len`) overload of `SocketAddrParse`. `spec` +/// need not be NUL-terminated; exactly `len` bytes are considered. The +/// shared IP/port parsers are NUL-terminated scanners, so this copies +/// the view into a bounded stack buffer before delegating to the zstr +/// arm. Same contract as the other arms. +/// +/// SUCCESS : Returns true; `*out` populated. +/// FAILURE : Returns false; `*out` zeroed. Silent (no log) — caller is +/// expected to chain into DNS for the hostname case. A view +/// too long to name a valid host:port also returns false. +/// +/// TAGS: Socket, Parse, Address +/// +bool socket_addr_parse_cstr(SocketAddr *out, Zstr spec, size len, SocketKind kind); +#define SocketAddrParse(...) OVERLOAD(SocketAddrParse, __VA_ARGS__) +#define SocketAddrParse_3(out, spec, kind) \ + _Generic( \ + (spec), \ + Str *: socket_addr_parse_str((out), (const Str *)(spec), (kind)), \ + Zstr: socket_addr_parse_zstr((out), (Zstr)(spec), (kind)), \ + char *: socket_addr_parse_zstr((out), (Zstr)(spec), (kind)) \ ) +#define SocketAddrParse_4(out, spec, len, kind) socket_addr_parse_cstr((out), (Zstr)(spec), (len), (kind)) /// /// Render a `SocketAddr` back into a "ip:port" string. IPv6 addresses diff --git a/Include/Misra/Sys/SymbolResolver.h b/Include/Misra/Sys/SymbolResolver.h index 8a76f44..51fc6f6 100644 --- a/Include/Misra/Sys/SymbolResolver.h +++ b/Include/Misra/Sys/SymbolResolver.h @@ -30,6 +30,7 @@ # include #endif #include +#include #include #include #include @@ -73,8 +74,8 @@ typedef struct ResolvedSymbol { } ResolvedSymbol; typedef struct ResolverCacheEntry { - Zstr path; // borrowed from ProcMaps.raw - Elf elf; + Str path; // owned NUL-terminated copy of the mapping path (the ProcMaps slice is not terminated) + Elf elf; // Sidecar debug file found via .gnu_debuglink or .note.gnu.build-id. // Populated lazily for stripped binaries that have an installed // -dbg package or a debug file alongside them. When `has_sidecar` diff --git a/Source/Misra/ParserCombinator.c b/Source/Misra/ParserCombinator.c new file mode 100644 index 0000000..b831b58 --- /dev/null +++ b/Source/Misra/ParserCombinator.c @@ -0,0 +1,87 @@ +/// file : ParserCombinator.c +/// author : Siddharth Mishra (admin@brightprogrammer.in) +/// This is free and unencumbered software released into the public domain. +/// +/// The fundamental byte-reader parsers the DSL delivers out of the box, so a +/// binary grammar composes `PcMatch(PcU32BE, &field)` / `PcMatch(PcI32BE, &f)` +/// without hand-writing a wrapper per field. Each reads one fixed-width field +/// from the `BufIter` cursor at the stated endianness through Buf's direct +/// `BufRead{U,I}*`; a short buffer fails the rule (never aborts). Defined here +/// rather than inline so they live outside every grammar's translation unit -- +/// which is why they see the context only as an opaque `struct PcParserCtx *` +/// (unused; a grammar's context is tagged `struct PcParserCtx` so the pointer +/// lines up at the call). + +#include +#include +#include +#include + +#define PC_DELIVER_READER(Name, Type, Reader) \ + PcParserStatus pc_parser_##Name(BufIter *in, struct PcParserCtx *ctx, Type *value) { \ + (void)ctx; \ + PC_BYTE_ATOM(Reader(in, value)); \ + } + +PC_DELIVER_READER(PcU8, u8, BufReadU8) +PC_DELIVER_READER(PcU16BE, u16, BufReadU16BE) +PC_DELIVER_READER(PcU16LE, u16, BufReadU16LE) +PC_DELIVER_READER(PcU32BE, u32, BufReadU32BE) +PC_DELIVER_READER(PcU32LE, u32, BufReadU32LE) +PC_DELIVER_READER(PcU64BE, u64, BufReadU64BE) +PC_DELIVER_READER(PcU64LE, u64, BufReadU64LE) + +PC_DELIVER_READER(PcI8, i8, BufReadI8) +PC_DELIVER_READER(PcI16BE, i16, BufReadI16BE) +PC_DELIVER_READER(PcI16LE, i16, BufReadI16LE) +PC_DELIVER_READER(PcI32BE, i32, BufReadI32BE) +PC_DELIVER_READER(PcI32LE, i32, BufReadI32LE) +PC_DELIVER_READER(PcI64BE, i64, BufReadI64BE) +PC_DELIVER_READER(PcI64LE, i64, BufReadI64LE) + +#undef PC_DELIVER_READER + +static Zstr pc_level_word(PcReportLevel level) { + switch (level) { + case PC_REPORT_ERROR : + return "error"; + case PC_REPORT_WARN : + return "warning"; + default : + return "note"; + } +} + +void PcReportsRender(Str *out, Str *src, PcReports *reports) { + const char *bytes = StrBegin(src); + u64 n = StrLen(src); + for (u64 r = 0; r < VecLen(reports); r++) { + PcReport rep = VecAt(reports, r); + + // The source line the span sits on, bounded by a newline -- or a NUL, since + // a parser may terminate a borrowed slice in place -- on either side. + u64 line_start = rep.start; + while (line_start > 0 && bytes[line_start - 1] != '\n' && bytes[line_start - 1] != '\0') + line_start--; + u64 line_end = rep.start; + while (line_end < n && bytes[line_end] != '\n' && bytes[line_end] != '\0') + line_end++; + u64 span_end = rep.end < line_end ? rep.end : line_end; + while (span_end > rep.start && (bytes[span_end - 1] == ' ' || bytes[span_end - 1] == '\t')) + span_end--; + + StrAppendFmt(out, "{}: {}\n", pc_level_word(rep.level), rep.message); + StrPushBackR(out, ' '); + StrPushBackR(out, ' '); + for (u64 c = line_start; c < line_end; c++) + StrPushBackR(out, bytes[c]); + StrPushBackR(out, '\n'); + StrPushBackR(out, ' '); + StrPushBackR(out, ' '); + for (u64 c = line_start; c < rep.start; c++) + StrPushBackR(out, ' '); + for (u64 c = rep.start; c < span_end; c++) + StrPushBackR(out, '^'); + StrPushBackR(out, '\n'); + } +} diff --git a/Source/Misra/Parsers/Elf.c b/Source/Misra/Parsers/Elf.c index d123c2a..a6af1a4 100644 --- a/Source/Misra/Parsers/Elf.c +++ b/Source/Misra/Parsers/Elf.c @@ -535,6 +535,19 @@ bool elf_open(Elf *out, Zstr path, Allocator *alloc) { return ElfOpenFromMemory(out, &data); } +bool elf_open_n(Elf *out, Zstr path, size len, Allocator *alloc) { + if (!out || !path || !alloc) { + LOG_FATAL("ElfOpen: NULL argument (contract violation)"); + } + Buf data = BufInit(alloc); + if (FileReadAndClose(path, len, &data) < 0) { + BufDeinit(&data); + LOG_ERROR("ElfOpen: failed to read {} path bytes", len); + return false; + } + return ElfOpenFromMemory(out, &data); +} + void ElfDeinit(Elf *self) { if (!self) return; diff --git a/Source/Misra/Parsers/Http.c b/Source/Misra/Parsers/Http.c index c04e48b..15b7f74 100644 --- a/Source/Misra/Parsers/Http.c +++ b/Source/Misra/Parsers/Http.c @@ -76,6 +76,18 @@ HttpHeader *http_headers_find_str(HttpHeaders *headers, const Str *key) { return http_headers_find_zstr(headers, StrBegin(key)); } +HttpHeader *http_headers_find_cstr(HttpHeaders *headers, Zstr key, size key_len) { + if (!headers || !key) { + LOG_FATAL("invalid arguments"); + } + VecForeachPtr(headers, header) { + if (StrLen(&header->key) == key_len && 0 == ZstrCompareN(StrBegin(&header->key), key, key_len)) { + return header; + } + } + return NULL; +} + // --------------------------------------------------------------------------- // HttpRequest // --------------------------------------------------------------------------- @@ -141,7 +153,9 @@ Zstr http_request_parse_zstr(HttpRequest *req, Zstr in) { // RFC 7230 § 3.2.5 lets servers cap per-message header count; the // attacker-controlled stream otherwise grows req->headers without // bound. Match common reverse-proxy ceilings (nginx/haproxy ~100). - enum { HTTP_REQUEST_HEADERS_MAX = 100 }; + enum { + HTTP_REQUEST_HEADERS_MAX = 100 + }; while (true) { Zstr line_start = cursor; @@ -184,6 +198,23 @@ Zstr http_request_parse_str(HttpRequest *req, const Str *in) { return http_request_parse_zstr(req, StrBegin(in)); } +Zstr http_request_parse_cstr(HttpRequest *req, Zstr in, size in_len) { + if (!req || !req->allocator || !in) { + LOG_FATAL("invalid arguments"); + } + Str staged = StrInit(req->allocator); + if (!StrPushBackMany(&staged, in, in_len)) { + LOG_ERROR("failed to stage http request input"); + StrDeinit(&staged); + return in; + } + Zstr base = StrBegin(&staged); + Zstr cursor = http_request_parse_zstr(req, base); + Zstr result = (cursor == base) ? in : (in + (cursor - base)); + StrDeinit(&staged); + return result; +} + void HttpRequestDeinit(HttpRequest *req) { if (!req) { LOG_FATAL("invalid arguments"); @@ -448,6 +479,26 @@ HttpResponse *http_respond_with_file_str( } return http_respond_with_file_zstr(response, status, content_type, (Zstr)StrBegin(filepath)); } + +HttpResponse *http_respond_with_file_cstr( + HttpResponse *response, + HttpResponseCode status, + HttpContentType content_type, + Zstr filepath, + size filepath_len +) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!filepath || filepath_len >= sizeof(buf)) { + LOG_ERROR("invalid or oversized file path"); + return NULL; + } + MemCopy(buf, filepath, filepath_len); + buf[filepath_len] = '\0'; + return http_respond_with_file_zstr(response, status, content_type, buf); +} #endif Str http_response_serialize(const HttpResponse *response, Allocator *alloc) { diff --git a/Source/Misra/Parsers/KvConfig.c b/Source/Misra/Parsers/KvConfig.c index 67d7c68..e70edd4 100644 --- a/Source/Misra/Parsers/KvConfig.c +++ b/Source/Misra/Parsers/KvConfig.c @@ -352,6 +352,20 @@ Str *kvconfig_get_ptr_zstr(KvConfig *cfg, Zstr key) { return value; } +Str *kvconfig_get_ptr_cstr(KvConfig *cfg, Zstr key, size len) { + Str lookup = {0}; + Str *value = NULL; + + if (!cfg || !key) { + return NULL; + } + + lookup = StrInitFromCstr(key, len, MapAllocator(cfg)); + value = kvconfig_get_ptr_str(cfg, &lookup); + StrDeinit(&lookup); + return value; +} + Str kvconfig_get_str(KvConfig *cfg, const Str *key) { Str *value = kvconfig_get_ptr_str(cfg, key); @@ -372,6 +386,16 @@ Str kvconfig_get_zstr(KvConfig *cfg, Zstr key) { return StrInitFromCstr(StrBegin(value), StrLen(value), MapAllocator(cfg)); } +Str kvconfig_get_cstr(KvConfig *cfg, Zstr key, size len) { + Str *value = kvconfig_get_ptr_cstr(cfg, key, len); + + if (!value) { + return StrInit(MapAllocator(cfg)); + } + + return StrInitFromCstr(StrBegin(value), StrLen(value), MapAllocator(cfg)); +} + bool kvconfig_contains_str(KvConfig *cfg, const Str *key) { return kvconfig_get_ptr_str(cfg, key) != NULL; } @@ -380,6 +404,10 @@ bool kvconfig_contains_zstr(KvConfig *cfg, Zstr key) { return kvconfig_get_ptr_zstr(cfg, key) != NULL; } +bool kvconfig_contains_cstr(KvConfig *cfg, Zstr key, size len) { + return kvconfig_get_ptr_cstr(cfg, key, len) != NULL; +} + bool kvconfig_get_bool_str(KvConfig *cfg, const Str *key, bool *value) { Str *str = kvconfig_get_ptr_str(cfg, key); @@ -400,6 +428,16 @@ bool kvconfig_get_bool_zstr(KvConfig *cfg, Zstr key, bool *value) { return kvconfig_parse_bool_value(str, value); } +bool kvconfig_get_bool_cstr(KvConfig *cfg, Zstr key, size len, bool *value) { + Str *str = kvconfig_get_ptr_cstr(cfg, key, len); + + if (!str) { + return false; + } + + return kvconfig_parse_bool_value(str, value); +} + bool kvconfig_get_i64_str(KvConfig *cfg, const Str *key, i64 *value) { Str *str = kvconfig_get_ptr_str(cfg, key); @@ -420,6 +458,16 @@ bool kvconfig_get_i64_zstr(KvConfig *cfg, Zstr key, i64 *value) { return kvconfig_parse_i64_value(str, value); } +bool kvconfig_get_i64_cstr(KvConfig *cfg, Zstr key, size len, i64 *value) { + Str *str = kvconfig_get_ptr_cstr(cfg, key, len); + + if (!str) { + return false; + } + + return kvconfig_parse_i64_value(str, value); +} + bool kvconfig_get_f64_str(KvConfig *cfg, const Str *key, f64 *value) { Str *str = kvconfig_get_ptr_str(cfg, key); @@ -439,3 +487,13 @@ bool kvconfig_get_f64_zstr(KvConfig *cfg, Zstr key, f64 *value) { return kvconfig_parse_f64_value(str, value); } + +bool kvconfig_get_f64_cstr(KvConfig *cfg, Zstr key, size len, f64 *value) { + Str *str = kvconfig_get_ptr_cstr(cfg, key, len); + + if (!str) { + return false; + } + + return kvconfig_parse_f64_value(str, value); +} diff --git a/Source/Misra/Parsers/MachO.c b/Source/Misra/Parsers/MachO.c index 745757d..5b3d6e9 100644 --- a/Source/Misra/Parsers/MachO.c +++ b/Source/Misra/Parsers/MachO.c @@ -471,6 +471,23 @@ bool macho_open(Macho *out, Zstr path, Allocator *alloc) { return MachoOpenFromMemory(out, &data); } +bool macho_open_n(Macho *out, Zstr path, size len, Allocator *alloc) { + if (!out || !path || !alloc) { + LOG_FATAL("MachoOpen: NULL argument (contract violation)"); + } + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (len >= sizeof(buf)) { + LOG_ERROR("MachoOpen: path length {} exceeds cap", (u64)len); + return false; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return macho_open(out, buf, alloc); +} + void MachoDeinit(Macho *self) { if (!self) return; @@ -493,6 +510,21 @@ const MachoSection *macho_find_section(const Macho *self, Zstr segment, Zstr sec return NULL; } +const MachoSection * + macho_find_section_cstr(const Macho *self, Zstr segment, size segment_len, Zstr section, size section_len) { + if (!self || !segment || !section) + return NULL; + for (size i = 0; i < VecLen(&self->sections); ++i) { + const MachoSection *s = VecPtrAt(&self->sections, i); + if (segment_len < sizeof(s->segment) && s->segment[segment_len] == '\0' && section_len < sizeof(s->section) && + s->section[section_len] == '\0' && ZstrCompareN(s->segment, segment, segment_len) == 0 && + ZstrCompareN(s->section, section, section_len) == 0) { + return s; + } + } + return NULL; +} + // Mach-O nlist_64 entries carry no size. Pick the symbol with the // largest `value <= vaddr`, then bound it by the next symbol in the // same section (or the section end). N_STAB entries are skipped: diff --git a/Source/Misra/Parsers/Pdb.c b/Source/Misra/Parsers/Pdb.c index 9be20c2..700dcd9 100644 --- a/Source/Misra/Parsers/Pdb.c +++ b/Source/Misra/Parsers/Pdb.c @@ -18,8 +18,8 @@ // --------------------------------------------------------------------------- static const u8 MSF_MAGIC_7[32] = {'M', 'i', 'c', 'r', 'o', 's', 'o', 'f', 't', ' ', 'C', - '/', 'C', '+', '+', ' ', 'M', 'S', 'F', ' ', '7', '.', - '0', '0', '\r', '\n', '\x1A', 'D', 'S', '\0', '\0', '\0'}; + '/', 'C', '+', '+', ' ', 'M', 'S', 'F', ' ', '7', '.', + '0', '0', '\r', '\n', '\x1A', 'D', 'S', '\0', '\0', '\0'}; enum { SUPERBLOCK_SIZE = 56, @@ -754,6 +754,19 @@ bool pdb_open(Pdb *out, Zstr path, Allocator *alloc) { return PdbOpenFromMemory(out, &data); } +bool pdb_open_n(Pdb *out, Zstr path, size len, Allocator *alloc) { + if (!out || !path || !alloc) { + LOG_FATAL("PdbOpen: NULL argument (contract violation)"); + } + Buf data = BufInit(alloc); + if (FileReadAndClose(path, len, &data) < 0) { + BufDeinit(&data); + LOG_ERROR("PdbOpen: failed to read {} path bytes", len); + return false; + } + return PdbOpenFromMemory(out, &data); +} + void PdbDeinit(Pdb *self) { if (!self) return; diff --git a/Source/Misra/Parsers/Pe.c b/Source/Misra/Parsers/Pe.c index 8073879..a69dc58 100644 --- a/Source/Misra/Parsers/Pe.c +++ b/Source/Misra/Parsers/Pe.c @@ -566,6 +566,19 @@ bool pe_open(Pe *out, Zstr path, Allocator *alloc) { return PeOpenFromMemory(out, &data); } +bool pe_open_n(Pe *out, Zstr path, size len, Allocator *alloc) { + if (!out || !path || !alloc) { + LOG_FATAL("PeOpen: NULL argument (contract violation)"); + } + Buf data = BufInit(alloc); + if (FileReadAndClose(path, len, &data) < 0) { + BufDeinit(&data); + LOG_ERROR("PeOpen: failed to read {} path bytes", len); + return false; + } + return PeOpenFromMemory(out, &data); +} + void PeDeinit(Pe *self) { if (!self) return; @@ -592,6 +605,18 @@ const PeSection *pe_find_section_str(const Pe *self, const Str *name) { return pe_find_section_zstr(self, StrBegin(name)); } +const PeSection *pe_find_section_cstr(const Pe *self, Zstr name, size name_len) { + if (!self || !name) + return NULL; + for (size i = 0; i < VecLen(&self->sections); ++i) { + const PeSection *s = VecPtrAt(&self->sections, i); + if (name_len < sizeof(s->name) && s->name[name_len] == '\0' && ZstrCompareN(s->name, name, name_len) == 0) { + return s; + } + } + return NULL; +} + bool PeRvaToOffset(const Pe *self, u32 rva, u64 *out_offset) { if (!self || !out_offset) return false; diff --git a/Source/Misra/Parsers/ProcMaps.c b/Source/Misra/Parsers/ProcMaps.c index 3b591b7..871f760 100644 --- a/Source/Misra/Parsers/ProcMaps.c +++ b/Source/Misra/Parsers/ProcMaps.c @@ -15,8 +15,15 @@ /// inode : decimal (we ignore) /// path : optional; anonymous mappings have no path field /// -/// Paths can contain spaces — we treat everything after the inode -/// field's trailing whitespace as the path, up to the line terminator. +/// The whole file is a grammar on the parser-combinator DSL over a +/// `StrIter` (the default cursor -- no `#define PC_ITER`): each field is +/// its own rule, `ProcMapLine` a line, and `ProcMapLines` the file -- +/// which decodes each line into an entry while folding in `min_addr`, +/// and fails on the first line it cannot parse (naming the cause in the +/// diagnostics sink), so the loader just runs it. Paths can contain +/// spaces -- everything after the inode token runs to the line terminator +/// and is copied into the entry's owned `Str`, so the transient parse +/// buffer is dropped once loading returns. #include @@ -27,175 +34,175 @@ #include #include +#include + +// The context carries only the diagnostics sink the `PcReport*` macros append to: +// a malformed line is recorded here, not silently dropped. Nothing else is +// ambient -- every field is a return value. Reports are not rolled back on +// backtracking (a recorded error stays recorded), so the savepoint is a no-op. +typedef struct PcParserCtx { + PcReports reports; + Allocator *alloc; // copies each entry's path into an owned Str +} PcParserCtx; +typedef struct { + u8 unused; +} PcParserCtxMark; +static PcParserCtxMark PcParserCtxSnapshot(PcParserCtx *ctx) { + (void)ctx; + return (PcParserCtxMark) {0}; +} +static void PcParserCtxRollback(PcParserCtx *ctx, PcParserCtxMark mark) { + (void)ctx; + (void)mark; +} + // --------------------------------------------------------------------------- -// Field parsers +// Field grammar // --------------------------------------------------------------------------- -static int hex_digit_value(char c) { - if (c >= '0' && c <= '9') - return c - '0'; - if (c >= 'a' && c <= 'f') - return 10 + (c - 'a'); - if (c >= 'A' && c <= 'F') - return 10 + (c - 'A'); - return -1; +// One hex digit -> its 0..15 value (lower or upper case). +PcParser(HexDigit, u8) { + PcSatisfyChar(c, (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { + *value = (u8)(c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10); + } } -// Parse a hex run at the cursor. Advances the iter past the digits. -// Returns false if no digits are consumed. -static bool parse_hex_u64(StrIter *si, u64 *out) { - u64 v = 0; - int consumed = 0; - char c; - while (StrIterPeek(si, &c)) { - int d = hex_digit_value(c); - if (d < 0) - break; - v = (v << 4) | (u64)d; - StrIterMustNext(si); - ++consumed; +// A hex run -> u64. One-or-more, so an empty field (no digits) fails the rule. +PcParser(HexU64, u64) { + u8 d = 0; + u64 v = 0; + PcSeq() { + PcMatchOneOrMore(HexDigit, &d) { + v = (v << 4) | (u64)d; + } + *value = v; } - if (!consumed) - return false; - *out = v; - return true; } -static bool expect_char(StrIter *si, char want) { - char c; - if (!StrIterPeek(si, &c) || c != want) - return false; - StrIterMustNext(si); - return true; +// Inter-field whitespace (spaces/tabs), any amount including none. +PcRecognizer(SpaceCh) { + PcSatisfyChar(c, c == ' ' || c == '\t') {} +} +PcRecognizer(Spaces) { + PcRecognizeZeroOrMore(SpaceCh); } -static void skip_ws(StrIter *si) { - char c; - while (StrIterPeek(si, &c) && (c == ' ' || c == '\t')) { - StrIterMustNext(si); - } +// The '-' between start and end. +PcRecognizer(Dash) { + PcSatisfyChar(c, c == '-') {} } -// Read one "non-whitespace blob" (the dev/inode tokens). Just skip it. -static void skip_token(StrIter *si) { - char c; - while (StrIterPeek(si, &c) && c != ' ' && c != '\t' && c != '\n') { - StrIterMustNext(si); +// The four permission characters -> a ProcMapPerms bitmask. Position is +// significant: r/w/x/p set their bit, anything else (a '-' or 's') clears it. +PcParser(PermCh, char) { + PcSatisfyChar(c, c == 'r' || c == 'w' || c == 'x' || c == 'p' || c == 's' || c == '-') { + *value = c; } } - -// --------------------------------------------------------------------------- -// Line decode -// --------------------------------------------------------------------------- - -// Parses one `/proc/self/maps` line from `si`. On success the path is -// NUL-terminated in place (the trailing '\n' becomes '\0'), and -// `out->path` aliases the iter's underlying buffer. The iter is -// advanced past the line terminator so the next call resumes at the -// next line. -static bool parse_one_line(StrIter *si, ProcMapEntry *out) { - u64 start = 0, ende = 0, offset = 0; - if (!parse_hex_u64(si, &start)) - return false; - if (!expect_char(si, '-')) - return false; - if (!parse_hex_u64(si, &ende)) - return false; - skip_ws(si); - - // perms: 4 chars - if (StrIterRemainingLength(si) < 4) - return false; - u32 perms = 0; +PcParser(Perms, u32) { char p0 = 0, p1 = 0, p2 = 0, p3 = 0; - StrIterMustPeekAt(si, 0, &p0); - StrIterMustPeekAt(si, 1, &p1); - StrIterMustPeekAt(si, 2, &p2); - StrIterMustPeekAt(si, 3, &p3); - if (p0 == 'r') - perms |= PROC_MAP_PERM_READ; - if (p1 == 'w') - perms |= PROC_MAP_PERM_WRITE; - if (p2 == 'x') - perms |= PROC_MAP_PERM_EXEC; - if (p3 == 'p') - perms |= PROC_MAP_PERM_PRIVATE; - StrIterMustMove(si, 4); - skip_ws(si); - - if (!parse_hex_u64(si, &offset)) - return false; - skip_ws(si); + PcSeq() { + PcMatch(PermCh, &p0); + PcMatch(PermCh, &p1); + PcMatch(PermCh, &p2); + PcMatch(PermCh, &p3); + *value = (u32)((p0 == 'r' ? PROC_MAP_PERM_READ : 0u) | (p1 == 'w' ? PROC_MAP_PERM_WRITE : 0u) | + (p2 == 'x' ? PROC_MAP_PERM_EXEC : 0u) | (p3 == 'p' ? PROC_MAP_PERM_PRIVATE : 0u)); + } +} - // dev_major:dev_minor — we don't care, but the field must be there. - skip_token(si); - skip_ws(si); +// A whitespace-delimited token (the dev and inode fields, which we only skip). +PcRecognizer(TokenCh) { + PcSatisfyChar(c, c != ' ' && c != '\t' && c != '\n') {} +} +PcRecognizer(Token) { + PcRecognizeOneOrMore(TokenCh); +} - // inode — skip. - skip_token(si); - skip_ws(si); +// The line terminator, consumed after the path has been captured. +PcRecognizer(NewlineCh) { + PcSatisfyChar(c, c == '\n') {} +} - // path — optional, runs to end-of-line. We replace the newline - // with \0 in place so the path is a usable C string aliasing the - // iter's backing buffer. - size path_start_pos = StrIterIndex(si); - char c; - while (StrIterPeek(si, &c) && c != '\n') { - StrIterMustNext(si); +// One `/proc/self/maps` line's fields -> an entry. Any field failing fails the +// whole rule. +PcParser(ProcMapLine, ProcMapEntry) { + PcSeq() { + PcMatch(HexU64, &value->start); + PcExpect(Dash); + PcMatch(HexU64, &value->end); + PcRecognize(Spaces); + PcMatch(Perms, &value->perms); + PcRecognize(Spaces); + PcMatch(HexU64, &value->file_offset); + PcRecognize(Spaces); + PcExpect(Token); // dev_major:dev_minor + PcRecognize(Spaces); + PcExpect(Token); // inode + PcRecognize(Spaces); + // Path: the rest of the line. PcCaptureUntil hands back a (pointer, + // length) slice into the parse buffer; we copy it into an owned, + // NUL-terminated Str after the newline is consumed, so a line that fails + // to terminate never leaks a half-built path. + Zstr pc_path = NULL; + u64 pc_path_len = 0; + PcCaptureUntil(c, c == '\n', &pc_path, &pc_path_len); + PcRecognize(NewlineCh); + value->path = StrInitFromCstr(pc_path, pc_path_len, ctx->alloc); } - size line_terminator_pos = StrIterIndex(si); - - out->start = start; - out->end = ende; - out->perms = perms; - out->file_offset = offset; - out->path = (Zstr)StrIterDataAt(si, path_start_pos); // may be empty if anonymous +} - if (line_terminator_pos < StrIterLength(si) && *StrIterDataAt(si, line_terminator_pos) == '\n') { - // intentional bypass: in-place mutation of the iter's backing - // buffer to NUL-terminate the path slice we just exposed via - // `out->path`. Iter accessors are read-only; no public mutator - // covers single-byte writes to the underlying storage. - *StrIterDataAt(si, line_terminator_pos) = '\0'; - StrIterMustNext(si); +// The whole file: decode each line into `value->entries` (which the caller +// pre-initialized), folding `min_addr` in the same pass. This is not a compiler +// -- the first line the grammar cannot parse records its cause in the sink and +// faults (FAILED, carrying the consumed bit from wherever it got to), rather than +// skipping on; the caller fixes the input and reparses. +PcParser(ProcMapLines, ProcMaps) { + ProcMapEntry e = {0}; + PcSeq() { + PcMatchZeroOrMore(ProcMapLine, &e) { + if (!VecPushBackR(&value->entries, e)) { + StrDeinit(&e.path); + PcReject(); + } + if (VecLen(&value->entries) == 1 || e.start < value->min_addr) + value->min_addr = e.start; + } + // The zero-or-more stops at the first line it cannot parse; anything left + // means that line is malformed -- name the cause and fault. + PcFailIfNotEof("malformed /proc/self/maps line"); } - return true; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- -// Parse `out->raw` (already populated + NUL-terminated) into `entries` and -// `min_addr`. Shared by every loader. Deinits `out` on allocation failure. -static bool proc_maps_parse_raw(ProcMaps *out) { - // `StrResize`/`StrReserve` keep a NUL sentinel at index `length`, so path - // slices that alias the buffer are usable as `Zstr`. - StrIter si = StrIterFromStr(out->raw); - while (StrIterRemainingLength(&si)) { - ProcMapEntry e = {0}; - if (!parse_one_line(&si, &e)) { - // Skip past whatever line we couldn't parse. - char c; - while (StrIterPeek(&si, &c) && c != '\n') { - StrIterMustNext(&si); - } - if (StrIterRemainingLength(&si)) { - StrIterMustNext(&si); - } - continue; - } - if (!VecPushBackR(&out->entries, e)) { - ProcMapsDeinit(out); - return false; +// Parse `raw` (already populated + NUL-terminated) into `entries` and `min_addr` +// through the `ProcMapLines` grammar, each entry owning a copy of its path. A +// line the grammar cannot parse fails the whole load: render its cause under a +// caret to the log and return false (`out` left zeroed). An allocation failure +// fails the same way, without a recorded cause. `raw` is the caller's transient +// buffer -- it is not retained past this call. +static bool proc_maps_parse(ProcMaps *out, Str *raw) { + StrIter si = StrIterFromStr(*raw); + PcParserCtx cx; + cx.reports = VecInitT(cx.reports, VecAllocator(&out->entries)); + cx.alloc = VecAllocator(&out->entries); + PcParserCtx *ctx = &cx; + + bool ok = (PcRun(ProcMapLines, &si, out) & PC_PARSER_STATUS_SUCCESS); + if (!ok && VecLen(&cx.reports) > 0) { + StrInitStack(diag, 256) { + PcReportsRender(&diag, raw, &cx.reports); + LOG_ERROR("ProcMaps: {}", diag); } } + VecDeinit(&cx.reports); - // Cache the lowest mapped address so callers don't rescan the vector. - for (u64 i = 0; i < VecLen(&out->entries); ++i) { - const ProcMapEntry *e = VecPtrAt(&out->entries, i); - if (i == 0 || e->start < out->min_addr) - out->min_addr = e->start; + if (!ok) { + ProcMapsDeinit(out); + return false; } return true; } @@ -227,30 +234,35 @@ bool proc_maps_load(ProcMaps *out, Allocator *alloc) { LOG_FATAL("ProcMapsLoad: NULL argument"); } MemSet(out, 0, sizeof(*out)); - out->raw = StrInit(alloc); out->entries = VecInitT(out->entries, alloc); + Str raw = StrInit(alloc); // `/proc/self/maps` reports stat-size 0 because it's generated by the // kernel on read, so we loop-read into a growing buffer ourselves. File f = FileOpen("/proc/self/maps", "rb"); if (!FileIsOpen(&f)) { LOG_ERROR("ProcMapsLoad: FileOpen(/proc/self/maps) failed"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - bool ok = proc_maps_read_all(&f, &out->raw); + bool ok = proc_maps_read_all(&f, &raw); FileClose(&f); if (!ok) { LOG_ERROR("ProcMapsLoad: FileRead failed"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - if (StrLen(&out->raw) == 0) { + if (StrLen(&raw) == 0) { LOG_ERROR("ProcMapsLoad: /proc/self/maps was empty"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - return proc_maps_parse_raw(out); + bool parsed = proc_maps_parse(out, &raw); + StrDeinit(&raw); + return parsed; } bool proc_maps_load_from_file(ProcMaps *out, File *f, Allocator *alloc) { @@ -258,19 +270,23 @@ bool proc_maps_load_from_file(ProcMaps *out, File *f, Allocator *alloc) { LOG_FATAL("ProcMapsLoadFrom: NULL argument"); } MemSet(out, 0, sizeof(*out)); - out->raw = StrInit(alloc); out->entries = VecInitT(out->entries, alloc); + Str raw = StrInit(alloc); if (!FileIsOpen(f)) { LOG_ERROR("ProcMapsLoadFrom: file is not open"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - if (!proc_maps_read_all(f, &out->raw)) { + if (!proc_maps_read_all(f, &raw)) { LOG_ERROR("ProcMapsLoadFrom: FileRead failed"); + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - return proc_maps_parse_raw(out); + bool parsed = proc_maps_parse(out, &raw); + StrDeinit(&raw); + return parsed; } bool proc_maps_load_from_bytes(ProcMaps *out, const u8 *bytes, u64 len, Allocator *alloc) { @@ -278,23 +294,29 @@ bool proc_maps_load_from_bytes(ProcMaps *out, const u8 *bytes, u64 len, Allocato LOG_FATAL("ProcMapsLoadFrom: NULL argument"); } MemSet(out, 0, sizeof(*out)); - out->raw = StrInit(alloc); out->entries = VecInitT(out->entries, alloc); + Str raw = StrInit(alloc); if (len) { - if (!StrReserve(&out->raw, len + 1)) { + if (!StrReserve(&raw, len + 1)) { + StrDeinit(&raw); ProcMapsDeinit(out); return false; } - MemCopy(StrEnd(&out->raw), bytes, len); - StrResize(&out->raw, len); + MemCopy(StrEnd(&raw), bytes, len); + StrResize(&raw, len); } - return proc_maps_parse_raw(out); + bool parsed = proc_maps_parse(out, &raw); + StrDeinit(&raw); + return parsed; } void ProcMapsDeinit(ProcMaps *self) { if (!self) return; - StrDeinit(&self->raw); + for (u64 i = 0; i < VecLen(&self->entries); ++i) { + ProcMapEntry *e = VecPtrAt(&self->entries, i); + StrDeinit(&e->path); + } VecDeinit(&self->entries); MemSet(self, 0, sizeof(*self)); } diff --git a/Source/Misra/Parsers/Tzif.c b/Source/Misra/Parsers/Tzif.c index 4fbdd59..c6752d8 100644 --- a/Source/Misra/Parsers/Tzif.c +++ b/Source/Misra/Parsers/Tzif.c @@ -2,13 +2,18 @@ /// author : Siddharth Mishra (admin@brightprogrammer.in) /// This is free and unencumbered software released into the public domain. /// -/// TZif (`/etc/localtime`) offset resolver backing `ClockLocal`. See -/// `Tzif.h` for the contract and the scope note. All on-disk decoding -/// goes through `BufReadFmt` big-endian (`{>Nr}`) directives -- TZif is -/// a big-endian format (RFC 8536) -- so there are no packed structs and -/// no manual byteswaps, matching the rest of the Parsers/ family. Every -/// read is the non-`Must` `BufReadFmt` / `IterMove`, which return false -/// on a short file instead of aborting: `/etc/localtime` is untrusted. +/// TZif (`/etc/localtime`) offset resolver backing `ClockLocal`. See `Tzif.h` +/// for the contract and the scope note. Written on the parser combinator DSL +/// over a `BufIter` (`#define PC_ITER BufIter`): TZif is big-endian (RFC 8536). +/// The grammar reads the header (a produced value) and the version chooses the +/// block (`Dispatch`); each version arm reads transition times at its own width +/// (`FindBestTransitionTimeV1` vs `...V2`), so the width is a control-flow +/// decision, not a field anyone checks. Each resolution phase is its own parser +/// that takes what it needs and returns what it found, the arrays stream through +/// `PcMatchExactlyN`, and the context holds only the one ambient fact the phases +/// read down the stack: the instant being resolved. Every read is +/// bounds-checked and fails the rule (never aborts) on a short file -- +/// `/etc/localtime` is untrusted -- surfacing as a soft `false`. #include @@ -17,25 +22,12 @@ #include #include -// TZif fixed-layout header (RFC 8536 4.1): "TZif" magic, 1-byte -// version, 15 reserved bytes, then six big-endian u32 counts. 44 bytes. -#define TZIF_HEADER_SIZE 44 - -// The six u32 counts, read in one shot. -#define FMT_TZIF_COUNTS_BE \ - "{>4r}" /* isutcnt */ \ - "{>4r}" /* isstdcnt */ \ - "{>4r}" /* leapcnt */ \ - "{>4r}" /* timecnt */ \ - "{>4r}" /* typecnt */ \ - "{>4r}" /* charcnt */ - -// One ttinfo record (RFC 8536 4.2): i32 utoff, u8 isdst, u8 desigidx. -#define FMT_TZIF_TTINFO_BE \ - "{>4r}" /* utoff */ \ - "{>1r}" /* isdst */ \ - "{>1r}" /* desigidx */ +#define PC_ITER BufIter +#include +// TZif fixed-layout header (RFC 8536 4.1): "TZif" magic, version, 15 reserved +// bytes, then six big-endian u32 counts. Produced by `Header`, threaded into the +// block parsers as an input. typedef struct { u8 version; u32 isutcnt; @@ -46,144 +38,202 @@ typedef struct { u32 charcnt; } TzifHeader; -static bool tzif_read_header(BufIter *it, TzifHeader *h) { - if ((u64)IterRemainingLength(it) < TZIF_HEADER_SIZE) - return false; - - // "TZif" magic matched inline; a non-TZif file fails the read here. - u8 ver = 0; - if (!BufReadFmt(it, "TZif{>1r}", ver)) - return false; - if (!IterMove(it, 15)) // reserved - return false; +// One count-prefixed array to scan for a record at a selected index: `count` +// records, keep the one at `sel` (-1 = nothing selected). +typedef struct { + u32 count; + i64 sel; +} PhaseIn; - if (!BufReadFmt(it, FMT_TZIF_COUNTS_BE, h->isutcnt, h->isstdcnt, h->leapcnt, h->timecnt, h->typecnt, h->charcnt)) - return false; - h->version = ver; - return true; +// One ttinfo record's decoded fields. +typedef struct { + i32 utoff; + u8 isdst; +} TtInfoVal; + +// The one ambient fact the phases read down the call stack: the instant being +// resolved. Everything a phase *produces* is a return value, not a field here, +// and the block's time width is decided by which version arm runs, not stored. +typedef struct PcParserCtx { + i64 unix_seconds; +} PcParserCtx; + +// Backtracking savepoint: the context is a flat value, so a mark is a copy of it +// and rollback restores it -- a `PcChoice` arm leaves nothing behind. +typedef PcParserCtx PcParserCtxMark; +static PcParserCtxMark PcParserCtxSnapshot(PcParserCtx *ctx) { + return *ctx; +} +static void PcParserCtxRollback(PcParserCtx *ctx, PcParserCtxMark mark) { + *ctx = mark; } -// Size of a version-1 data block, used only to skip it on the way to -// the version-2+ block. v1 transition times and leap records are 4 and -// 4+4 bytes respectively. +// Size of a version-1 data block, used only to skip it on the way to the +// version-2+ block (transition times 4 bytes, leap records 4+4). static u64 tzif_v1_data_size(const TzifHeader *h) { return (u64)h->timecnt * 4 + (u64)h->timecnt * 1 + (u64)h->typecnt * 6 + (u64)h->charcnt * 1 + (u64)h->leapcnt * 8 + (u64)h->isstdcnt * 1 + (u64)h->isutcnt * 1; } -// Resolve the offset from a data block whose transition times are -// `time_width` bytes (4 for v1, 8 for v2+). The cursor must sit at the -// start of the transition-time array. Streams through the block without -// storing it: find the last transition <= `unix_seconds`, take its -// type's utoff, with the first standard-time (isdst==0) ttinfo as the -// pre-history / past-table fallback. -static bool tzif_resolve(BufIter *it, u32 time_width, u32 timecnt, u32 typecnt, i64 unix_seconds, i32 *out_offset) { - if (typecnt == 0) - return false; +// The magic and a generic byte skip. +PcRecognizer(TzifMagic) { + PcSatisfyStr("TZif") {} +} +PcRecognizer(Skip, u64) { + PcSkipBytes(expect); +} + +// "TZif" + version + 15 reserved + six big-endian u32 counts. +PcParser(Header, TzifHeader) { + PcSeq() { + PcExpect(TzifMagic); + PcMatch(PcU8, &value->version); + PcExpect(Skip, 15); + PcMatch(PcU32BE, &value->isutcnt); + PcMatch(PcU32BE, &value->isstdcnt); + PcMatch(PcU32BE, &value->leapcnt); + PcMatch(PcU32BE, &value->timecnt); + PcMatch(PcU32BE, &value->typecnt); + PcMatch(PcU32BE, &value->charcnt); + } +} - // Phase 1: transitions are ascending; remember the index of the - // last one not in the future. - bool found = false; - i64 best = 0; - for (u32 i = 0; i < timecnt; ++i) { - i64 t = 0; - if (time_width == 8) { - if (!BufReadFmt(it, "{>8r}", t)) - return false; - } else { - i32 t32 = 0; - if (!BufReadFmt(it, "{>4r}", t32)) - return false; - t = t32; +// One ttinfo record: utoff (signed), the isdst flag, and a designation index we +// skip. `PcI32BE` reads straight into the signed field -- no unsigned temporary. +PcParser(TtInfo, TtInfoVal) { + PcSeq() { + PcMatch(PcI32BE, &value->utoff); + PcMatch(PcU8, &value->isdst); + PcExpect(Skip, 1); + } +} + +// Phase 1: transitions are ascending; the last index not in the future, or -1 if +// they are all ahead of the instant. The time is signed and its width is the +// version's (32-bit v1, 64-bit v2) -- so the two arms read via `PcI32BE` and +// `PcI64BE`, and that reader choice is the only difference between them. +PcParser(FindBestTransitionTimeV1, u32, i64) { + i32 t = 0; + PcSeq() { + *value = -1; + PcMatchExactlyN(expect, i, PcI32BE, &t) { + if (t <= ctx->unix_seconds) // i32 t sign-extends against the i64 instant + *value = (i64)i; } - if (t <= unix_seconds) { - found = true; - best = (i64)i; + } +} +PcParser(FindBestTransitionTimeV2, u32, i64) { + i64 t = 0; + PcSeq() { + *value = -1; + PcMatchExactlyN(expect, i, PcI64BE, &t) { + if (t <= ctx->unix_seconds) + *value = (i64)i; } } +} - // Phase 2: the type-index array is `timecnt` bytes; pick the one at - // `best` and step the cursor to the array's end either way. - u8 chosen = 0; - bool have_chosen = false; - if (found) { - if (best > 0 && !IterMove(it, best)) - return false; - if (!BufReadFmt(it, "{>1r}", chosen)) - return false; - have_chosen = true; - i64 rest = (i64)timecnt - best - 1; - if (rest > 0 && !IterMove(it, rest)) - return false; - } else if (timecnt > 0 && !IterMove(it, (i64)timecnt)) { - return false; +// Phase 2: the type-index array; the index recorded at `sel`, or -1 when there +// was no transition to select. +PcParser(PickTransitionType, PhaseIn, i64) { + u8 tix = 0; + PcSeq() { + *value = -1; + PcMatchExactlyN(expect.count, i, PcU8, &tix) { + if ((i64)i == expect.sel) + *value = (i64)tix; + } } +} - // Phase 3: ttinfo records. Grab the chosen type's utoff; remember - // the first standard-time offset for the fallback. - bool have_std = false; - i32 std_off = 0; - bool have_off = false; - i32 off = 0; - for (u32 i = 0; i < typecnt; ++i) { - i32 utoff = 0; - u8 isdst = 0, desigidx = 0; - if (!BufReadFmt(it, FMT_TZIF_TTINFO_BE, utoff, isdst, desigidx)) - return false; - if (have_chosen && i == (u32)chosen) { - off = utoff; - have_off = true; - } - if (!have_std && isdst == 0) { - std_off = utoff; - have_std = true; +// Phase 3: the ttinfo records; the selected type's utoff, else the first +// standard-time (isdst==0) type as the pre-history fallback. No usable type -> +// the rule rejects. +PcParser(PickUtOffset, PhaseIn, i32) { + TtInfoVal tt = {0}; + i32 off = 0; + i32 std_off = 0; + bool have_off = false; + bool have_std = false; + PcSeq() { + PcMatchExactlyN(expect.count, i, TtInfo, &tt) { + if ((i64)i == expect.sel) { + off = tt.utoff; + have_off = true; + } + if (!have_std && tt.isdst == 0) { + std_off = tt.utoff; + have_std = true; + } } + if (have_off) + *value = off; + else if (have_std) + *value = std_off; + else + PcReject(); } +} - if (have_off) { - *out_offset = off; - return true; +// Version dispatch. v1: resolve the block right here (4-byte times). v2+: skip +// the v1 block, read the v2 header, resolve its block (8-byte times). Each arm's +// guard rejects before consuming, so the choice falls through cleanly. +PcParser(ResolveV1, TzifHeader, i32) { + i64 best = 0; + i64 chosen = 0; + PcSeq() { + if (expect.version >= '2') + PcReject(); + PcMatch(FindBestTransitionTimeV1, expect.timecnt, &best); + PcMatch(PickTransitionType, ((PhaseIn) {expect.timecnt, best}), &chosen); + PcMatch(PickUtOffset, ((PhaseIn) {expect.typecnt, chosen}), value); + } +} +PcParser(ResolveV2, TzifHeader, i32) { + TzifHeader h2 = {0}; + i64 best = 0; + i64 chosen = 0; + PcSeq() { + if (expect.version < '2') + PcReject(); + PcExpect(Skip, tzif_v1_data_size(&expect)); + PcMatch(Header, &h2); + PcMatch(FindBestTransitionTimeV2, h2.timecnt, &best); + PcMatch(PickTransitionType, ((PhaseIn) {h2.timecnt, best}), &chosen); + PcMatch(PickUtOffset, ((PhaseIn) {h2.typecnt, chosen}), value); + } +} +PcParser(Dispatch, TzifHeader, i32) { + PcChoice() { + PcAlt(ResolveV1, expect, value); + PcAlt(ResolveV2, expect, value); } - if (have_std) { - *out_offset = std_off; - return true; +} + +// TZif: the fixed header, then the version-appropriate data block. +PcParser(Tzif, i32) { + TzifHeader h = {0}; + PcSeq() { + PcMatch(Header, &h); + PcMatch(Dispatch, h, value); } - return false; } bool TzifOffsetFromBuf(const u8 *data, size len, i64 unix_seconds, i32 *out_offset_seconds) { if (!data || !out_offset_seconds) return false; - BufIter it = BufIterFromMemory((u8 *)data, len); - TzifHeader h1; - if (!tzif_read_header(&it, &h1)) { - LOG_ERROR("Tzif: malformed header"); - return false; - } + BufIter in = BufIterFromMemory((u8 *)data, len); + PcParserCtx cx = {.unix_seconds = unix_seconds}; + PcParserCtx *ctx = &cx; + i32 offset = 0; - bool ok; - i32 offset = 0; - if (h1.version >= '2') { - // Skip the v1 data block, then parse the v2+ header and its - // 8-byte-time data block (the authoritative one). - if (!IterMove(&it, (i64)tzif_v1_data_size(&h1))) { - LOG_ERROR("Tzif: truncated v1 data block"); - return false; - } - TzifHeader h2; - if (!tzif_read_header(&it, &h2)) { - LOG_ERROR("Tzif: malformed v2 header"); - return false; - } - ok = tzif_resolve(&it, 8, h2.timecnt, h2.typecnt, unix_seconds, &offset); - } else { - ok = tzif_resolve(&it, 4, h1.timecnt, h1.typecnt, unix_seconds, &offset); + if (!(PcRun(Tzif, &in, &offset) & PC_PARSER_STATUS_SUCCESS)) { + LOG_ERROR("Tzif: malformed or unresolvable TZif data"); + return false; } - - if (ok) - *out_offset_seconds = offset; - return ok; + *out_offset_seconds = offset; + return true; } bool TzifLocalOffsetSeconds(i64 unix_seconds, i32 *out_offset_seconds, Allocator *alloc) { diff --git a/Source/Misra/Std/Container/List.c b/Source/Misra/Std/Container/List.c index b2f68c2..9e3c2ef 100644 --- a/Source/Misra/Std/Container/List.c +++ b/Source/Misra/Std/Container/List.c @@ -547,8 +547,8 @@ GenericListNode *get_node_for_list_iteration(GenericList *list, GenericListNode return get_node_random_access(list, node, nidx, (i64)target_idx - (i64)nidx); } -bool list_insert_one_l(GenericList *list, const void *item_copy, void *source, u64 item_size, u64 idx) { - return list_zero_source_on_success(list, source, item_size, insert_into_list(list, item_copy, item_size, idx)); +bool list_insert_one_l(GenericList *list, void *source, u64 item_size, u64 idx) { + return list_zero_source_on_success(list, source, item_size, insert_into_list(list, source, item_size, idx)); } bool list_insert_one_r(GenericList *list, const void *item_copy, u64 item_size, u64 idx) { diff --git a/Source/Misra/Std/Container/Vec.c b/Source/Misra/Std/Container/Vec.c index 2113c2f..6ce3de9 100644 --- a/Source/Misra/Std/Container/Vec.c +++ b/Source/Misra/Std/Container/Vec.c @@ -538,16 +538,9 @@ void validate_vec(const GenericVec *v) { ((GenericVec *)(void *)v)->__magic &= ~MAGIC_VALIDATED_BIT; } -bool vec_insert_one_l( - GenericVec *vec, - const void *item_copy, - void *source, - size item_size, - size idx, - bool preserve_order -) { - bool success = preserve_order ? insert_range_into_vec(vec, item_copy, item_size, idx, 1) : - insert_range_fast_into_vec(vec, item_copy, item_size, idx, 1); +bool vec_insert_one_l(GenericVec *vec, void *source, size item_size, size idx, bool preserve_order) { + bool success = preserve_order ? insert_range_into_vec(vec, source, item_size, idx, 1) : + insert_range_fast_into_vec(vec, source, item_size, idx, 1); return vec_zero_source_on_success(vec, source, item_size, success); } diff --git a/Source/Misra/Std/File.c b/Source/Misra/Std/File.c index 1c4703f..86bf625 100644 --- a/Source/Misra/Std/File.c +++ b/Source/Misra/Std/File.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -163,6 +164,23 @@ File file_open(Zstr path, Zstr mode) { #endif } +File file_open_n(Zstr path, size len, Zstr mode) { + // `path` is a fixed-length view; the OS open() needs a NUL-terminated + // C-string, so copy into a stack buffer bounded by the platform path cap + // (a path longer than this cannot be opened anyway). + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + File f = {0}; + return f; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return file_open(buf, mode); +} + File FileFromFd(i32 fd) { File f = {0}; #if PLATFORM_WINDOWS @@ -494,6 +512,23 @@ i64 file_read_and_close_to_str(Zstr path, Str *out) { return file_read_and_close_to_buf(path, (Buf *)out); } +i64 file_read_and_close_to_buf_n(Zstr path, size len, Buf *out) { + if (!path || !out) { + LOG_FATAL("FileReadAndClose: NULL argument (contract violation)"); + } + File f = file_open_n(path, len, "rb"); + if (!FileIsOpen(&f)) { + return -1; + } + i64 got = file_read_to_buf(&f, out); + FileClose(&f); + return got; +} + +i64 file_read_and_close_to_str_n(Zstr path, size len, Str *out) { + return file_read_and_close_to_buf_n(path, len, (Buf *)out); +} + i64 file_write_and_close_from_bytes(Zstr path, const void *buf, u64 n) { if (!path || (!buf && n > 0)) { LOG_FATAL("FileWriteAndClose: NULL argument (contract violation)"); diff --git a/Source/Misra/Sys/Dir.c b/Source/Misra/Sys/Dir.c index 25fb561..43139ba 100644 --- a/Source/Misra/Sys/Dir.c +++ b/Source/Misra/Sys/Dir.c @@ -277,6 +277,19 @@ DirContents dir_get_contents(Zstr path, Allocator *alloc) { # error "dir_get_contents: unsupported platform/architecture (no direct-syscall path)" #endif +DirContents dir_get_contents_cstr(Zstr path, size len, Allocator *alloc) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return (DirContents)VecInit(alloc); + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_get_contents(buf, alloc); +} + // Cross-platform function to get file size i64 file_get_size(Zstr filename) { #if PLATFORM_WINDOWS @@ -329,6 +342,19 @@ i64 file_get_size(Zstr filename) { #endif } +i64 file_get_size_cstr(Zstr filename, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!filename || len >= sizeof(buf)) { + return -1; + } + MemCopy(buf, filename, len); + buf[len] = '\0'; + return file_get_size(buf); +} + // --------------------------------------------------------------------------- // FileRemove / DirRemove. Linux uses the direct-syscall path when // FEATURE_DIRECT_SYSCALL is set (x86_64 -> SYS_unlink/SYS_rmdir; @@ -366,6 +392,19 @@ i8 file_remove(Zstr path) { #endif } +i8 file_remove_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return file_remove(buf); +} + i8 dir_remove(Zstr path) { if (!path) { LOG_FATAL("DirRemove: NULL path"); @@ -396,6 +435,19 @@ i8 dir_remove(Zstr path) { #endif } +i8 dir_remove_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_remove(buf); +} + // --------------------------------------------------------------------------- // DirCreate / DirCreateAll / DirRemoveAll. Linux x86_64 has SYS_mkdir; // aarch64 dropped it -- use SYS_mkdirat with AT_FDCWD. Darwin keeps @@ -435,6 +487,19 @@ i8 dir_create(Zstr path) { #endif } +i8 dir_create_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_create(buf); +} + // Check whether the given path already exists as a directory. Used by // DirCreateAll to make EEXIST tolerant (idempotent). Avoids re-walking // the existing tree on the second invocation. @@ -508,6 +573,19 @@ i8 dir_create_all(Zstr path) { return ok; } +i8 dir_create_all_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_create_all(buf); +} + // Per-entry "parent/child" path buffer cap for the recursive removal // loop. Kept well under 4 KiB on purpose: on macOS, Clang emits an // implicit `___chkstk_darwin` call in the prologue of any function @@ -575,3 +653,16 @@ i8 dir_remove_all(Zstr path) { } return DirRemove(path); } + +i8 dir_remove_all_cstr(Zstr path, size len) { + enum { + PATH_CAP = 4096 + }; + char buf[PATH_CAP]; + if (!path || len >= sizeof(buf)) { + return 0; + } + MemCopy(buf, path, len); + buf[len] = '\0'; + return dir_remove_all(buf); +} diff --git a/Source/Misra/Sys/Dns.c b/Source/Misra/Sys/Dns.c index 560d877..4766853 100644 --- a/Source/Misra/Sys/Dns.c +++ b/Source/Misra/Sys/Dns.c @@ -694,6 +694,22 @@ bool dns_resolve_5_str(DnsResolver *self, const Str *hostname, u16 port, SocketK return dns_resolve_5_zstr(self, StrBegin(hostname), port, kind, out); } +bool dns_resolve_6_cstr(DnsResolver *self, Zstr hostname, u64 hostname_len, u16 port, SocketKind kind, DnsAddrs *out) { + if (!self || !hostname || !out) { + return false; + } + if (hostname_len >= 256) { + LOG_ERROR("DnsResolve: hostname exceeds 255 bytes"); + return false; + } + bool ok = false; + StrInitStack(host, 256) { + StrPushBackMany(&host, hostname, hostname_len); + ok = dns_resolve_5_zstr(self, StrBegin(&host), port, kind, out); + } + return ok; +} + bool dns_resolve_4_vec_str(DnsResolver *self, const Str *spec, SocketKind kind, DnsAddrs *out) { if (!self || !spec || !out) { return false; diff --git a/Source/Misra/Sys/Socket.c b/Source/Misra/Sys/Socket.c index 9c6b145..6714950 100644 --- a/Source/Misra/Sys/Socket.c +++ b/Source/Misra/Sys/Socket.c @@ -455,6 +455,29 @@ bool socket_addr_parse_str(SocketAddr *out, const Str *spec, SocketKind kind) { return socket_addr_parse_zstr(out, StrBegin(spec), kind); } +bool socket_addr_parse_cstr(SocketAddr *out, Zstr spec, size len, SocketKind kind) { + if (!out) { + LOG_FATAL("SocketAddrParse: out is NULL"); + } + MemSet(out, 0, sizeof(*out)); + + // `spec` is a fixed-length view. The shared parsers (split_host_port, + // parse_ipv4/parse_ipv6, parse_port) all scan to a NUL terminator, so the + // view is copied into a bounded stack buffer and NUL-terminated before + // delegating to the zstr arm. A spec longer than this cannot name a valid + // host:port anyway. + enum { + SPEC_CAP = 256 + }; + char buf[SPEC_CAP]; + if (!spec || len >= sizeof(buf)) { + return false; + } + MemCopy(buf, spec, len); + buf[len] = '\0'; + return socket_addr_parse_zstr(out, buf, kind); +} + Str socket_addr_format(const SocketAddr *addr, Allocator *alloc) { Str out = StrInit(alloc); if (!addr || addr->length == 0) { diff --git a/Source/Misra/Sys/SymbolResolver.c b/Source/Misra/Sys/SymbolResolver.c index 53a907b..dab0a8f 100644 --- a/Source/Misra/Sys/SymbolResolver.c +++ b/Source/Misra/Sys/SymbolResolver.c @@ -140,35 +140,34 @@ static bool try_open_sidecar(Zstr main_path, const Elf *main, Elf *out, Allocato // Cache management // --------------------------------------------------------------------------- -static ResolverCacheEntry *resolver_cache_find_or_open(SymbolResolver *self, Zstr path) { +static ResolverCacheEntry *resolver_cache_find_or_open(SymbolResolver *self, Zstr path, u64 path_len) { for (u64 i = 0; i < VecLen(&self->cache); ++i) { ResolverCacheEntry *e = VecPtrAt(&self->cache, i); - if (e->path == path) { - return e; - } - // Some `/proc/self/maps` lines share the same path string in - // different positions of the raw buffer if the kernel - // generated separate copies — fall back to string compare. - if (e->path && path && ZstrCompare(e->path, path) == 0) { + // The cache owns its path copy; match it against the caller's path. + if (StrCmp(&e->path, path, path_len) == 0) { return e; } } ResolverCacheEntry entry; MemSet(&entry, 0, sizeof(entry)); - entry.path = path; - if (!ElfOpen(&entry.elf, path, self->allocator)) { + // Keep the cache's own copy of the path so the entry (and the public + // module_path that borrows it) stay valid independent of the ProcMaps. + entry.path = StrInitFromCstr(path, path_len, self->allocator); + if (!ElfOpen(&entry.elf, &entry.path, self->allocator)) { + StrDeinit(&entry.path); return NULL; } // Best-effort sidecar lookup. Silent failure is fine — we'll just // resolve against whatever the main file has. - if (try_open_sidecar(path, &entry.elf, &entry.sidecar, self->allocator)) { + if (try_open_sidecar(StrBegin(&entry.path), &entry.elf, &entry.sidecar, self->allocator)) { entry.has_sidecar = true; } if (!VecPushBackR(&self->cache, entry)) { if (entry.has_sidecar) ElfDeinit(&entry.sidecar); ElfDeinit(&entry.elf); + StrDeinit(&entry.path); return NULL; } return VecPtrAt(&self->cache, VecLen(&self->cache) - 1); @@ -219,6 +218,7 @@ void SymbolResolverDeinit(SymbolResolver *self) { ElfDeinit(&e->sidecar); } ElfDeinit(&e->elf); + StrDeinit(&e->path); } VecDeinit(&self->cache); ProcMapsDeinit(&self->maps); @@ -265,10 +265,10 @@ bool SymbolResolverFindFde( u64 addr = (u64)runtime_addr; const ProcMapEntry *entry = ProcMapsFindByAddr(&self->maps, addr); - if (!entry || !entry->path || entry->path[0] == '\0') + if (!entry || StrEmpty(&entry->path)) return false; - ResolverCacheEntry *cache_entry = resolver_cache_find_or_open(self, entry->path); + ResolverCacheEntry *cache_entry = resolver_cache_find_or_open(self, StrBegin(&entry->path), StrLen(&entry->path)); if (!cache_entry) return false; // p_vaddr-space bias (not the file-offset shortcut) -- see resolver_load_bias. @@ -348,11 +348,11 @@ bool SymbolResolverResolve(SymbolResolver *self, void *runtime_addr, ResolvedSym u64 addr = (u64)runtime_addr; const ProcMapEntry *entry = ProcMapsFindByAddr(&self->maps, addr); - if (!entry || !entry->path || entry->path[0] == '\0') { + if (!entry || StrEmpty(&entry->path)) { return false; } - ResolverCacheEntry *cache_entry = resolver_cache_find_or_open(self, entry->path); + ResolverCacheEntry *cache_entry = resolver_cache_find_or_open(self, StrBegin(&entry->path), StrLen(&entry->path)); if (!cache_entry) { return false; } @@ -361,7 +361,7 @@ bool SymbolResolverResolve(SymbolResolver *self, void *runtime_addr, ResolvedSym // equals the mapping start, so the absolute base still falls out. u64 load_base = resolver_load_bias(&cache_entry->elf, entry->start, entry->file_offset, addr); - out->module_path = entry->path; + out->module_path = StrBegin(&cache_entry->path); out->module_base = load_base; u64 file_relative = addr - load_base; diff --git a/Tests/Parsers/ProcMaps.c b/Tests/Parsers/ProcMaps.c index 8d3740a..440c9e1 100644 --- a/Tests/Parsers/ProcMaps.c +++ b/Tests/Parsers/ProcMaps.c @@ -15,14 +15,19 @@ static int pm2_marker_fn(int x) { } // Parse crafted `/proc/self/maps`-format TEXT through the public LoadFrom -// seam. The bytes are copied into `m->raw`, so we can feed string literals -// and still assert exact parsed fields -- no live `/proc` involved. Returns -// the loader's own success flag (true even when a line is malformed: a bad -// line is skipped, not a hard failure). +// seam. The bytes are parsed through a transient buffer, so we can feed string +// literals and still assert exact parsed fields -- no live `/proc` involved. Returns +// the loader's own success flag -- false if any line is malformed, since the +// parse fails (naming the cause) on the first line it cannot decode. static bool pm_load_text(ProcMaps *m, Zstr text, DefaultAllocator *alloc) { return ProcMapsLoadFrom(m, text, ZstrLen(text), alloc); } +// `path` is an owned, NUL-terminated Str -- compare it against a literal. +static bool path_eq(const ProcMapEntry *e, Zstr want) { + return ZstrCompare(StrBegin(&e->path), want) == 0; +} + bool test_procmaps_load(void) { DefaultAllocator alloc = DefaultAllocatorInit(); ProcMaps maps; @@ -316,12 +321,14 @@ bool test_pm2_find_no_overrun_past_length(void) { DebugAllocator alloc = DebugAllocatorInit(); ProcMaps pm; MemSet(&pm, 0, sizeof(pm)); - pm.raw = StrInit(ALLOCATOR_OF(&alloc)); pm.entries = VecInitT(pm.entries, ALLOCATOR_OF(&alloc)); - ProcMapEntry e0 = {.start = 0x1000, .end = 0x2000, .perms = 0, .file_offset = 0, .path = ""}; - ProcMapEntry e1 = {.start = 0x3000, .end = 0x4000, .perms = 0, .file_offset = 0, .path = ""}; - ProcMapEntry e2 = {.start = 0x5000, .end = 0x6000, .perms = 0, .file_offset = 0, .path = ""}; + ProcMapEntry e0 = {.start = 0x1000, .end = 0x2000, .perms = 0, .file_offset = 0}; + ProcMapEntry e1 = {.start = 0x3000, .end = 0x4000, .perms = 0, .file_offset = 0}; + ProcMapEntry e2 = {.start = 0x5000, .end = 0x6000, .perms = 0, .file_offset = 0}; + e0.path = StrInit(ALLOCATOR_OF(&alloc)); + e1.path = StrInit(ALLOCATOR_OF(&alloc)); + e2.path = StrInit(ALLOCATOR_OF(&alloc)); bool pushed = VecPushBackR(&pm.entries, e0) && VecPushBackR(&pm.entries, e1) && VecPushBackR(&pm.entries, e2); if (!pushed) { @@ -348,13 +355,13 @@ bool test_pm2_find_no_overrun_past_length(void) { } // -------------------------------------------------------------------------- -// ProcMapsDeinit: actually frees the raw buffer + entries vector. +// ProcMapsDeinit: actually frees every entry's owned path + entries vector. // -------------------------------------------------------------------------- // Under a DebugAllocator, every allocation made by the load must be -// released by Deinit. If `StrDeinit(&self->raw)` is removed, the raw -// buffer (kilobytes) leaks and the live-allocation count stays above -// baseline. Asserting the count returns to baseline kills the +// released by Deinit. If `StrDeinit(&e->path)` is dropped from the per-entry +// loop, those owned path copies leak and the live-allocation count stays +// above baseline. Asserting the count returns to baseline kills the // removed-Deinit mutation observably. bool test_pm2_deinit_releases_all(void) { DebugAllocator dbg = DebugAllocatorInit(); @@ -436,8 +443,7 @@ bool test_pm_parse_full_line_fields(void) { if (ok) { const ProcMapEntry *e = VecPtrAt(&m.entries, 0); ok = e->start == 0x1000ULL && e->end == 0x2000ULL && e->file_offset == 0xdeadULL && - e->perms == (u32)(PROC_MAP_PERM_READ | PROC_MAP_PERM_EXEC | PROC_MAP_PERM_PRIVATE) && - ZstrCompare(e->path, "/x/y") == 0; + e->perms == (u32)(PROC_MAP_PERM_READ | PROC_MAP_PERM_EXEC | PROC_MAP_PERM_PRIVATE) && path_eq(e, "/x/y"); } ProcMapsDeinit(&m); @@ -491,23 +497,18 @@ bool test_pm_parse_addr_with_nine(void) { } // An address field with NO hex digits (here an empty start, the line opening -// straight on the '-') is malformed and the whole line is skipped. Pins the -// "no digits consumed -> reject" guard in the hex reader: a reader that -// reports success on an empty run would accept the line and emit a bogus -// zero-based entry, so asserting ZERO entries is the kill. +// straight on the '-') is malformed, so the load fails. Pins the "no digits +// consumed -> reject" guard in the hex reader: a reader that reports success on +// an empty run would accept the line, so asserting the load FAILS is the kill. bool test_pm_parse_rejects_empty_start_field(void) { DefaultAllocator alloc = DefaultAllocatorInit(); ProcMaps m; - if (!pm_load_text(&m, "-2000 r-xp 0 0:0 0 /x\n", &alloc)) { - DefaultAllocatorDeinit(&alloc); - return false; - } - - bool ok = VecLen(&m.entries) == 0; + bool failed = !pm_load_text(&m, "-2000 r-xp 0 0:0 0 /x\n", &alloc); - ProcMapsDeinit(&m); + if (!failed) // a failed load already freed + zeroed `m`; only clean up on success + ProcMapsDeinit(&m); DefaultAllocatorDeinit(&alloc); - return ok; + return failed; } // min_addr is the lowest start across ALL entries, regardless of file order. @@ -534,28 +535,74 @@ bool test_pm_parse_min_addr_descending(void) { return ok; } -// A line that fails to parse is skipped whole, and the NEXT valid line still -// decodes correctly from its true start. If the skip-to-newline scan stops -// short, the recovery resumes mid-garbage and the following entry comes out -// with the wrong start (e.g. a dropped leading digit) -- so pinning the -// recovered entry's exact start proves the bad line was consumed cleanly. -bool test_pm_parse_skips_malformed_line(void) { +// A line the grammar cannot parse fails the whole load -- this is not a +// compiler, so the first bad line stops the parse rather than being skipped over +// to reach a later good line. Feeding "garbage" ahead of a well-formed line and +// asserting the load FAILS pins that: a skip-and-continue reader would instead +// return the good entry. +bool test_pm_parse_fails_on_malformed_line(void) { DefaultAllocator alloc = DefaultAllocatorInit(); ProcMaps m; - if (!pm_load_text(&m, "garbage line here\n3000-4000 r-xp 0 0:0 0 /good\n", &alloc)) { - DefaultAllocatorDeinit(&alloc); - return false; - } + bool failed = !pm_load_text(&m, "garbage line here\n3000-4000 r-xp 0 0:0 0 /good\n", &alloc); - bool ok = VecLen(&m.entries) == 1; - if (ok) { - const ProcMapEntry *e = VecPtrAt(&m.entries, 0); - ok = e->start == 0x3000ULL && e->end == 0x4000ULL && ZstrCompare(e->path, "/good") == 0; - } + if (!failed) // a failed load already freed + zeroed `m`; only clean up on success + ProcMapsDeinit(&m); + DefaultAllocatorDeinit(&alloc); + return failed; +} - ProcMapsDeinit(&m); +// Field-level fault tolerance: a well-formed line up to a point, then one bad +// field. The grammar has no partial-line recovery -- a bad field fails the line +// and thus the load. Each of these logs a caret diagnostic (see the [ERROR] +// lines when the suite runs); the assertion is just that the load fails. + +// A non-hex character in the END address fails the `end` field. +bool test_pm_parse_fails_on_bad_end(void) { + DefaultAllocator alloc = DefaultAllocatorInit(); + ProcMaps m; + bool failed = !pm_load_text(&m, "1000-XYZ r-xp 0 0:0 0 /x\n", &alloc); + + if (!failed) + ProcMapsDeinit(&m); DefaultAllocatorDeinit(&alloc); - return ok; + return failed; +} + +// A character outside `rwxp-s` in the permission field fails `perms`. +bool test_pm_parse_fails_on_bad_perms(void) { + DefaultAllocator alloc = DefaultAllocatorInit(); + ProcMaps m; + bool failed = !pm_load_text(&m, "1000-2000 rZxp 0 0:0 0 /x\n", &alloc); + + if (!failed) + ProcMapsDeinit(&m); + DefaultAllocatorDeinit(&alloc); + return failed; +} + +// A non-hex character in the file-offset field fails `offset`. +bool test_pm_parse_fails_on_bad_offset(void) { + DefaultAllocator alloc = DefaultAllocatorInit(); + ProcMaps m; + bool failed = !pm_load_text(&m, "1000-2000 r-xp ZZZ 0:0 0 /x\n", &alloc); + + if (!failed) + ProcMapsDeinit(&m); + DefaultAllocatorDeinit(&alloc); + return failed; +} + +// A line that ends before its offset/dev/inode fields fails: the next field has +// nothing to parse. +bool test_pm_parse_fails_on_truncated_line(void) { + DefaultAllocator alloc = DefaultAllocatorInit(); + ProcMaps m; + bool failed = !pm_load_text(&m, "1000-2000 r-xp\n", &alloc); + + if (!failed) + ProcMapsDeinit(&m); + DefaultAllocatorDeinit(&alloc); + return failed; } // The File loader must read to TRUE EOF, not stop at the first chunk. We write @@ -612,8 +659,7 @@ bool test_pm_parse_large_file_reads_all_chunks(void) { // The trailing high mapping lives past the first read chunk; a loader // that stopped early would never have parsed it. const ProcMapEntry *e = ProcMapsFindByAddr(&m, 0xdeadbeef050ULL); - ok = e != NULL && e->start == 0xdeadbeef000ULL && e->end == 0xdeadbeef100ULL && - ZstrCompare(e->path, "/late") == 0; + ok = e != NULL && e->start == 0xdeadbeef000ULL && e->end == 0xdeadbeef100ULL && path_eq(e, "/late"); ProcMapsDeinit(&m); } @@ -682,7 +728,11 @@ int main(void) { test_pm_parse_addr_with_nine, test_pm_parse_rejects_empty_start_field, test_pm_parse_min_addr_descending, - test_pm_parse_skips_malformed_line, + test_pm_parse_fails_on_malformed_line, + test_pm_parse_fails_on_bad_end, + test_pm_parse_fails_on_bad_perms, + test_pm_parse_fails_on_bad_offset, + test_pm_parse_fails_on_truncated_line, test_pm_parse_large_file_reads_all_chunks, test_pm_load_from_file_read_fail_frees_raw, }; diff --git a/Tests/Std/Io/Write.c b/Tests/Std/Io/Write.c index 0dbaf6e..2489ec7 100644 --- a/Tests/Std/Io/Write.c +++ b/Tests/Std/Io/Write.c @@ -4993,7 +4993,7 @@ static bool test_fwrite_roundtrip(void) { if (ok) { ok = ok && FWriteFmtLn(&f, "n={}", LVAL((i32)42)); FileClose(&f); - File r = FileOpen(StrBegin(&path), "r"); + File r = FileOpen(&path, "r"); if (FileIsOpen(&r)) { Str back = StrInit(&alloc); FileRead(&r, &back); @@ -5024,7 +5024,7 @@ static bool test_fwrite_empty_skipped(void) { ok = ok && FWriteFmt(&f, ""); ok = ok && FWriteFmt(&f, "X"); FileClose(&f); - File r = FileOpen(StrBegin(&path), "r"); + File r = FileOpen(&path, "r"); if (FileIsOpen(&r)) { Str back = StrInit(&alloc); FileRead(&r, &back); diff --git a/Tests/Std/Vec/Foreach.c b/Tests/Std/Vec/Foreach.c index ce2d24e..3d5ab06 100644 --- a/Tests/Std/Vec/Foreach.c +++ b/Tests/Std/Vec/Foreach.c @@ -16,6 +16,8 @@ bool test_vec_foreach_reverse(void); bool test_vec_foreach_reverse_idx(void); bool test_vec_foreach_ptr_reverse(void); bool test_vec_foreach_ptr_reverse_idx(void); +bool test_vec_foreach_early_break(void); +bool test_vec_foreach_reverse_early_break(void); bool test_vec_foreach_out_of_bounds_access(void); bool test_vec_foreach_idx_out_of_bounds_access(void); @@ -571,6 +573,70 @@ bool test_vec_foreach_idx_basic_out_of_bounds_access(void) { return true; } +// Early break: `break` inside a forward VecForeach must stop iteration immediately. +bool test_vec_foreach_early_break(void) { + WriteFmt("Testing early break through forward VecForeach variants\n"); + + typedef Vec(int) IntVec; + IntVec vec = VecInit(&alloc); + for (int i = 0; i < 5; i++) + VecPushBackR(&vec, i); // 0,1,2,3,4 + + bool result = true; + + int visited = 0, last = -1; + VecForeach(&vec, item) { + visited++; + last = item; + if (item == 2) + break; + } + result = result && (visited == 3) && (last == 2); + + visited = 0; + last = -1; + VecForeachPtr(&vec, item_ptr) { + visited++; + last = *item_ptr; + if (*item_ptr == 2) + break; + } + result = result && (visited == 3) && (last == 2); + + visited = 0; + VecForeachIdx(&vec, item, idx) { + visited++; + (void)item; + if (idx == 2) + break; + } + result = result && (visited == 3); + + VecDeinit(&vec); + return result; +} + +// Early break through the reverse variant (visits 4,3,2,1,0; break at 2 -> 3 visits). +bool test_vec_foreach_reverse_early_break(void) { + WriteFmt("Testing early break through reverse VecForeach\n"); + + typedef Vec(int) IntVec; + IntVec vec = VecInit(&alloc); + for (int i = 0; i < 5; i++) + VecPushBackR(&vec, i); + + int visited = 0, last = -1; + VecForeachReverse(&vec, item) { + visited++; + last = item; + if (item == 2) + break; + } + + VecDeinit(&vec); + return (visited == 3) && (last == 2); +} + // Main function that runs all tests int main(void) { alloc = DefaultAllocatorInit(); @@ -586,6 +652,8 @@ int main(void) { test_vec_foreach_reverse_idx, test_vec_foreach_ptr_reverse, test_vec_foreach_ptr_reverse_idx, + test_vec_foreach_early_break, + test_vec_foreach_reverse_early_break, test_vec_foreach_out_of_bounds_access, test_vec_foreach_idx_out_of_bounds_access, test_vec_foreach_idx_basic_out_of_bounds_access, diff --git a/Tests/Sys/SymbolResolver/Bias.c b/Tests/Sys/SymbolResolver/Bias.c index 4c8255b..36ac05d 100644 --- a/Tests/Sys/SymbolResolver/Bias.c +++ b/Tests/Sys/SymbolResolver/Bias.c @@ -32,10 +32,9 @@ static bool find_rw_mapping(Zstr path, u64 addr, u64 *map_start, u64 *map_file_o ProcMaps maps; bool got = false; if (ProcMapsLoad(&maps, ALLOCATOR_OF(&a))) { - for (u64 i = 0; i < VecLen(&maps.entries); ++i) { - const ProcMapEntry *m = VecPtrAt(&maps.entries, i); - if (m->path && ZstrCompare(m->path, path) == 0 && (m->perms & PROC_MAP_PERM_WRITE) && addr >= m->start && - addr < m->end) { + VecForeachPtr(&maps.entries, m) { + if (!StrEmpty(&m->path) && StrCmp(&m->path, path) == 0 && (m->perms & PROC_MAP_PERM_WRITE) && + addr >= m->start && addr < m->end) { *map_start = m->start; *map_file_offset = m->file_offset; got = true; diff --git a/Tests/Sys/SymbolResolver/SymbolResolver.c b/Tests/Sys/SymbolResolver/SymbolResolver.c index e2d98ce..c93e703 100644 --- a/Tests/Sys/SymbolResolver/SymbolResolver.c +++ b/Tests/Sys/SymbolResolver/SymbolResolver.c @@ -391,10 +391,9 @@ static bool find_other_module_addr(Zstr self_path, u64 *out_addr) { ProcMaps maps; bool got = false; if (ProcMapsLoad(&maps, ALLOCATOR_OF(&a))) { - for (u64 i = 0; i < VecLen(&maps.entries); ++i) { - const ProcMapEntry *m = VecPtrAt(&maps.entries, i); - if (m->path && m->path[0] == '/' && (m->perms & PROC_MAP_PERM_EXEC) && - ZstrCompare(m->path, self_path) != 0) { + VecForeachPtr(&maps.entries, m) { + if (!StrEmpty(&m->path) && StrBegin(&m->path)[0] == '/' && (m->perms & PROC_MAP_PERM_EXEC) && + StrCmp(&m->path, self_path) != 0) { *out_addr = m->start; got = true; break; diff --git a/meson.build b/meson.build index 31252fc..3be52d0 100644 --- a/meson.build +++ b/meson.build @@ -213,6 +213,7 @@ misra_std_sources = files( 'Source/Misra/Std/Memory.c', 'Source/Misra/Std/Zstr.c', 'Source/Misra/Std/DateTime.c', + 'Source/Misra/ParserCombinator.c', 'Source/Misra/Std/Allocator.c', 'Source/Misra/Std/Allocator/_Os.c', 'Source/Misra/Std/Allocator/Page.c', @@ -742,6 +743,22 @@ if opt_sys_dns ) endif +# calc: an immediate-mode parser-combinator calculator (Bin/CalC.c) -- the +# worked example / playground for Include/Misra/ParserCombinator.h. Built and +# kept green by the tree, but not installed: it demonstrates the DSL, it is not +# a shipping utility. Needs Map (variables) and File (the stdin REPL). +if opt_map and opt_file + executable( + 'calc', + files('Bin/CalC.c') + freestanding_extra_sources, + link_with: [misra_std], + c_args: common_c_args, + include_directories: inc_misra, + install: false, + link_args: freestanding_link_args + ) +endif + # Fuzzing harness pulls in List explicitly through Fuzz/Harness/ListInt.c. if opt_list executable(