diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 11:35:21 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-19 11:35:21 +0200 |
| commit | b084bba86fe9d394d6a5ea287161864e7695a609 (patch) | |
| tree | 3cbb56ffec7f5ba5947aa7fd7128372662b0ef97 /bin | |
| parent | a9464100bb02d74c90d585f8334f449b1fefa9d7 (diff) | |
| download | colitur-b084bba86fe9d394d6a5ea287161864e7695a609.tar.gz colitur-b084bba86fe9d394d6a5ea287161864e7695a609.zip | |
fix(cli): guard publish's IO, validate --dtstamp, and list all commands
publish's own mkdir_p/write_file (unlike every other IO path on this
branch) were unguarded: an unwritable --out parent raised a bare
Unix.Unix_error(EACCES,...) and an --out naming an existing file raised
ENOTDIR, both as uncaught exceptions with a stack trace rather than the
project's one-line "colitur: ..." form. The same defect class commit
6bd741b already fixed once for template reads -- --out is user input
too. Fixed by wrapping the whole publish_report call (not each
write_file site) in one handler for Unix.Unix_error and Sys_error,
mirroring why that earlier fix guarded the whole read and not only the
open. Added a cram case using a read-only directory inside the test's
own cram sandbox, not /tmp, so a failed cleanup cannot leave an
unwritable directory behind in a shared location.
--dtstamp was the only user string reaching output unescaped and
unvalidated: "--dtstamp hello" silently emitted an invalid
"DTSTAMP:hello", and a value carrying its own CRLF injected extra lines
into every VEVENT. Fixed by rejecting anything not matching RFC 5545's
UTC form (8 digits, 'T', 6 digits, 'Z') before either emit or publish
does anything else, one line to stderr, exit 2.
usage() was byte-unchanged from before the branch and listed only the
six pre-existing commands, omitting all four commands this branch added
(emit, table, render, publish). Added them; the three cram pins of the
exact usage string are updated to match.
Diffstat (limited to 'bin')
| -rw-r--r-- | bin/main.ml | 60 |
1 files changed, 58 insertions, 2 deletions
diff --git a/bin/main.ml b/bin/main.ml index 6f3cbd5..6f64582 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -389,7 +389,38 @@ let readings_report ~overlays y = resolved_year_report ~line:readings_line ~over file; XML: one document per year, the schema's own root is a single year; ICS: one VCALENDAR per year, valid to concatenate for a subscriber that reads multiple files). *) +(* [--dtstamp] is the only user string that reaches [emit]/[publish] output + unescaped and unvalidated (it becomes an ICS DTSTAMP: property value + directly, Colitur_render.Emit_ics.year's own [dtstamp] parameter) -- + every OTHER interpolated value in this project's output is either + escaped (Escape.apply) or engine-computed, never raw user input placed + straight into a line-oriented format. RFC 5545 section 3.3.5 defines + DATE-TIME's UTC form as exactly 8 digits, "T", 6 digits, "Z" + (e.g. "20270101T000000Z"); rejecting anything else is what stops + "--dtstamp hello" from silently emitting a malformed "DTSTAMP:hello" AND + what stops a value carrying its own CRLF (e.g. "X\r\nBEGIN:VEVENT\r\n...") + from being injected verbatim into every VEVENT -- a value shaped exactly + like the real form cannot contain either character. *) +let dtstamp_well_formed s = + let is_digit c = c >= '0' && c <= '9' in + String.length s = 16 + && String.for_all is_digit (String.sub s 0 8) + && s.[8] = 'T' + && String.for_all is_digit (String.sub s 9 6) + && s.[15] = 'Z' + +let check_dtstamp = function + | None -> () + | Some s when dtstamp_well_formed s -> () + | Some s -> + Printf.eprintf + "colitur: --dtstamp %S is not RFC 5545 UTC form (want 8 digits, 'T', 6 digits, 'Z', e.g. \ + 20270101T000000Z)\n" + s; + exit 2 + let emit_report ~format ~overlays ~dtstamp ~from_y ~to_y = + check_dtstamp dtstamp; if from_y > to_y then begin Printf.eprintf "colitur: --from %d is after --to %d\n" from_y to_y; exit 2 @@ -695,6 +726,7 @@ let index_html ~from_y ~to_y = Buffer.contents b let publish_report ~from_y ~to_y ~out ~overlays ~dtstamp ~prune = + check_dtstamp dtstamp; if from_y > to_y then begin Printf.eprintf "colitur: --from %d is after --to %d\n" from_y to_y; exit 2 @@ -978,7 +1010,9 @@ let print_help () = let usage () = prerr_endline "colitur: usage: colitur easter <year> | colitur temporal <year> | colitur day <year> | colitur \ - readings <year> | colitur check FILE | colitur new-overlay (try: colitur --help)"; + readings <year> | colitur emit --format FMT --from Y --to Y | colitur table --year Y --template \ + FILE | colitur render --template FILE --year Y | colitur publish --from Y --to Y --out DIR | \ + colitur check FILE | colitur new-overlay (try: colitur --help)"; exit 2 let with_year ys f = @@ -1383,5 +1417,27 @@ let () = exit 2 | Some from_ys, Some to_ys -> with_year from_ys (fun from_y -> - with_year to_ys (fun to_y -> publish_report ~from_y ~to_y ~out ~overlays ~dtstamp ~prune)))) + with_year to_ys (fun to_y -> + (* [publish_report] writes many files across a whole + year range ([mkdir_p]/[write_file], both above) -- + an unwritable [--out] parent (EACCES) or an [--out] + that names an existing plain file (ENOTDIR) raises + from deep inside that loop, same defect class as + the template read this project already guards + (commit 6bd741b): "--out" is user input too, and + the WHOLE call is guarded here rather than each + [write_file] site individually, for the same + reason that fix guarded the whole read and not + only the open. [Unix.mkdir] raises + [Unix.Unix_error] directly; [open_out_bin] + (stdlib, not the Unix module) wraps the same + underlying errno in [Sys_error] instead -- both + are real on this path, so both are caught. *) + try publish_report ~from_y ~to_y ~out ~overlays ~dtstamp ~prune with + | Unix.Unix_error (e, fn, arg) -> + Printf.eprintf "colitur: %s: %s: %s\n" fn arg (Unix.error_message e); + exit 2 + | Sys_error e -> + Printf.eprintf "colitur: %s\n" e; + exit 2)))) | _ -> usage ()) |
