fix(collab): image checks keep documents shareable and history replayable (#1086)

- The entry size cap is figcraft_doc::ImageData::MAX_BYTES (256 MiB), the
  cap the .fig, .sketch and REST importers already read images with (now
  one constant), instead of 32 MiB: an imported image larger than 32 MiB
  made the document unjoinable and its server snapshot unrestorable.
- History written before the checks (a restored or joined snapshot, a
  replayed server log, a file published to a server) applies without the
  image entries they refuse (docprops::without_refused_images), instead of
  the file not opening or a restart dropping every edit after the image.
- A key that already holds an image can't be given other bytes: producers
  key images by their bytes (the REST importer's placeholder keys always
  hold the same picture), and merge treats a key's image as never
  changing.
- REST import: an unresolved imageRef shaped like a content hash shares
  the rest-image-missing placeholder (the placeholder's bytes don't hash to
  it); ImageData::claims_content_hash is shared with collab.
- Tests: collab image_tests (4 new, size test uses a small cap), format
  image_tests (1 new).
- Docs: collab-protocol.md, collaboration-parity.md, file-format-parity.md.
This commit is contained in:
Christopher Linnett
2026-10-29 08:55:03 +08:00
parent f1c2ee81e4
commit 949d74f7ca
13 changed files with 144 additions and 46 deletions
+4 -1
View File
@@ -155,7 +155,10 @@ As built:
1–128 ASCII letters, digits, `-` or `_` (a saved file names the image `images/<key>`), a key
with the shape of a content hash (64 hex chars) must be exactly the lowercase sha256 hex of
those bytes (as the app writes them); a side is at most 65 535 px and the decoded bytes at
most 32 MiB, and base64 that can't decode is refused); a
most 256 MiB (`ImageData::MAX_BYTES`, what the importers read), base64 that can't decode is
refused, and a key that already holds an image can't be given other bytes; a snapshot or a
server log written before these checks opens without the images they refuse, rather than
failing, and a file published to a server is kept without them); a
board's timer and vote (whole values, `DocProp::Timer` and
`DocProp::Vote`) and the vote's ballots (`Entry::Ballot(voter)`, one per voter, so people voting
at once write different entries). The timer travels as its end time (or, paused, the time left),