Keep writes private, fetches bounded, drift honest
A review of the commits since 2026-10-06 found one security regression and several smaller problems; this fixes them, along with two parser holes found while validating, and rewrites the README's opening.
With a workspace root, a pull or push wrote the new contents through a temporary file created 0644 under the usual umask, and applied the destination's permissions only after writing it, so a private note was readable by other local users for the length of the write, or for good after a crash. The temporary is now 0600, as it already was without a root. A write that expects no file now refuses at placement too (no-replace rename, or a hard link), so a file created after the check is kept rather than replaced.
title_drift compared only the listed title with the current body, so a note whose title deliberately differs from its H1 reported drift on every sync, with a hint to rename it. It is now reported only when the last-synced body gave the listed title. Remote image fetches check the file signature on the first bytes instead of after the whole download, share one deadline across redirects, and no longer turn a URL containing "certificate" into a permanent TLS refusal. A %+1 escape in a note URL is refused instead of decoding as 0x01, and tab-indented front matter gives no title. The README now says when to choose this server over HackMD's hosted one, compared by their documented tool interfaces.
Verified with cargo test --all-targets --all-features (352 unit and 11 stdio tests), cargo clippy --all-targets --all-features -- -D warnings, cargo fmt --check, and a check of all targets on Rust 1.88; each of the eight commits builds, lints, and passes on its own. The into_stored rewrite was compared against the old two-pass form on all 87,381 strings up to length 8 over a, \r, \n, and a CJK character, and percent_decode on every two-character escape. New tests fail without their fix: the private temporary, the shared redirect deadline, the slow signature, and the declared length counting the bytes already read.
Not run: the live suites, so nothing here was measured against HackMD itself. The PowerShell commands in the README have not been run on Windows.
Summary by cubic
Keeps private note writes unreadable by other local users, reports title drift only when the last synced body gave the listed title, refuses non-images on their first bytes, and rewrites the README's opening to say when to choose this server over HackMD's hosted one.
Security and local writes
write_atomicnow writes through a 0600 temporary and, when the write expected no file, keeps whatever appeared after the check instead of replacing it; a created file stays private in both confined and unconfined paths, and permissions are lent only by a file the write expected.- Any link failure except an existing name now falls back to the pre-checked placement, so exFAT and FAT32 still create, replace, and refuse a changed file as intended.
- Remote images are type-checked on their first bytes under one deadline for the whole redirect chain, so a page that is not an image is refused before the rest downloads; a URL containing "certificate" no longer maps to a permanent TLS refusal, and past 64 pinned hosts one pin is dropped per new host.
Parsing and title drift
title_driftis reported only when the body last synced gave the listed title and no longer does; a deliberate difference is no longer flagged on every sync, and the baseline is parsed only on paths that report it.- A
%+1escape in a note URL is refused instead of decoding as byte 0x01, tab-indented front matter no longer yields a title, and hex and line endings are encoded in one pass. - A probe test now sets a title with repeated spaces and a tab and prints whether HackMD keeps, collapses, changes, or refuses it, so the whitespace allowance can be narrowed once measured; a refusal is an answer and no longer aborts the destructive test.