* craft-media: shared codec identifiers, one ByteSource, the testkit
The types a codec crate needs to describe a file and configure a
decoder, ahead of craft-audio:
- `Container`, `CodecId`, `PcmFormat` and `CodecParams` (`id.rs`),
always built, with optional serde.
- `codec_params()` on `isobmff::Track` and `matroska::Track`, both
returning `CodecParams` in the same form; `extradata` follows
Matroska's `CodecPrivate`.
- One `ByteSource` trait (`source.rs`), with `ReadSeekSource`;
`isobmff::ByteSource` and `matroska::ByteSource` are that trait.
- craft-media-testkit (`publish = false`): the ffmpeg oracle helpers
from craft-media's tests, so the codec crates share one copy.
The existing tests prove the refactor; the new types have their own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-media: APV extradata is the bare record; reseek after a poisoned lock
An MP4 apvC track gave CodecParams.extradata with the 4-byte FullBox header
in front, where Matroska's V_APV CodecPrivate is the bare
APVDecoderConfigurationRecord. ApvConfig::record_bytes serializes the record
alone and extradata uses it.
ReadSeekSource kept its tracked position after recovering a poisoned lock or
a failed seek, so a reader moved by a panic was read from the wrong offset
with no error. Both now force a seek on the next read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-media: MP4 codec params per sample description
Track::codec_params always described the first stsd entry, while each sample
selects its own through description_index. It now describes the first
sample's entry; codec_params_for gives the parameters of any entry, and
has_mixed_descriptions says whether one set covers every sample.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-media: test file-backed ByteSources; a File read past i64::MAX is an EOF
A File and a ReadSeekSource over a file now have the buffer's test: the same
bytes at the same offsets, and UnexpectedEof past the end. The test found
that a File read at an offset past i64::MAX failed with the OS's
InvalidInput instead of the UnexpectedEof the trait promises; it now
reports the end of the source like the other implementations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-media: Matroska codec params match MP4 for ALAC, DNx and Opus
Matroska's codec_params gave Unknown for A_ALAC and for DNxHD/DNxHR stored
as V_MS/VFW/FOURCC (AVdn, AVdh), where MP4 gives Alac and Dnxhd; ALAC's
extradata is the CodecPrivate, the ALACSpecificConfig MP4's alac box holds.
Opus now reports 48 kHz from Matroska as from MP4, whatever rate the muxer
declared.
ReadSeekSource clears the poison when it recovers the lock, so one
panicked read costs one recovery instead of a seek on every later read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-media: MP4 reports the MPEG audio layer the first frame names
An MP4 track with MPEG audio reported CodecId::Mp3 whatever the layer, since
mp4a + 0x69/0x6B names none (ffmpeg stores MP2 that way), while Matroska
reports Mp1/Mp2/Mp3 from its CodecID. open() now reads the frame header of
each such entry's first sample and codec_params() reports the layer it
names; without a valid header, QuickTime's .mp2 gives Mp2 and anything else
Mp3 as before. CodecConfig::Mp3 is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add craft-pcraft-format: the envelope of PhotoCraft's .pcraft bundle
The manifest around the document, with the document as a type parameter
(Manifest<D>), TileRef, the bundle's entry paths and hash check,
FORMAT_VERSION, the manifest decode limits with the nesting and item
scan, and the migration runner as a generic step list. Messages and
serialized field order are PhotoCraft's, so its saved manifests stay
byte-identical when it switches to these types.
Part of #59.
* ci: do not keep the checkout token in the pcraft-format workflow
The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.
* pcraft-format: build no entry name from an invalid hash
tile_path and blob_path return None unless the hash is 64 lowercase hex
digits, so a hash read from a file such as "../x" never becomes an entry
name. TileRef::path gives a tile's entry the same way.
The docs note that MAX_GROUP_DEPTH is a copy of PhotoCraft's constant,
and that check_manifest also refuses a manifest larger than
max_manifest_bytes, which PhotoCraft's check before a save does not.
* ci: pin actions/checkout by commit and Rust to 1.95 in the pcraft-format workflow
* pcraft-format: read_manifest is the one reader of a manifest
read_manifest(json, limits, steps, target) checks the JSON against the
limits, refuses a format_version newer than target, runs the migration
steps, and reads the document as D and the other envelope members.
read_manifest_with reads the document with the caller's function
instead. Manifest no longer implements Deserialize.
Error::TooNew holds the manifest's generator, so a message can name the
writer; its message is unchanged. TooNew, Manifest and TileRef are
#[non_exhaustive], and Manifest::new and TileRef::new build them.
TILES_DIR, BLOBS_DIR, COMPOSITE_DIR and OBJECT_SUFFIX name the bundle's
directories and the object suffix. The docs of max_blob_bytes and
max_total_bytes say the caller enforces them.
* pcraft-format: read_manifest reports errors in serde's struct order
read_manifest reads the envelope members and the document in the order
the parsed object holds them, and reports a missing member after the
members that are present. A manifest with several faults gives the same
error as a derived Deserialize of the whole manifest, as PhotoCraft
reads it today.
* pcraft-format: document that a missing or invalid format_version is Corrupt
read_manifest's docs and the README say that a missing or invalid
format_version gives Error::Corrupt from migrate, and that a missing
generator or document, or any other mistyped member, gives Error::Json.
A test covers a string and a fractional format_version.
* pcraft-format: read_manifest refuses preview names other than its own
A manifest's `thumbnail` and `composite` named any entry the reader then
opened, while tile entries go through `is_valid_hash`. `read_manifest`
now gives `Error::Corrupt` for a `thumbnail` other than `thumb.png` or a
`composite` other than `composite/preview.png`, the only names PhotoCraft
writes. The check runs after the members are read, so JSON errors keep
their order.
The reader's doc now says a missing or invalid `format_version` is
`Error::Corrupt`, from `migrate`, not `Error::Json`.
* Add craft-vectorcraft-format: the .vectorcraft envelope, with the document as a type parameter
File<D>, Head and Image hold the members VectorCraft writes, in its
order, with its skip rules: serde_json output of a File<D> is the JSON
VectorCraft writes, compact or pretty. Tail places entries after the
JSON from version 4 (INLINE_MAX bytes or more, after BLOB_MAGIC), and
split_tail and Image::bytes read them back. The crate also has the
version and name constants, check() for the format name and newer
versions, sniff() for plain and gzip files (the top-level "format"
member must be vectorcraft or drawcraft), and standard base64.
No dependency on any app; builds for wasm32-unknown-unknown.
* ci: do not keep the checkout token in the vectorcraft-format workflow
The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.
* vectorcraft-format: limit the bytes copied from the data after the JSON
Entries::bytes reads a file's entries and returns Error::TooBig once the
blob ranges it has copied add up to more than the data after the JSON
holds, so entries that name the same bytes again and again are refused,
as VectorCraft's reader refuses them.
A generator, preview or pdf of the wrong shape now reads as None in File
and Head, as VectorCraft opens such a file. File::tail gives a Tail for
the file's own version.
* ci: pin actions/checkout by commit and Rust to 1.95 in the vectorcraft-format workflow
* vectorcraft-format: document that base64 decode ignores the bits before the padding
decode reads a group such as Zh== like Zg==, and Zm9= like Zm8=, as
VectorCraft's own base64_decode does. The doc comment says so, and a
test pins both cases to the canonical bytes.
* vectorcraft-format: entries naming the same range share one buffer
VectorCraft's reader keeps the bytes of each [offset, length] it has
read and gives a repeated entry the same buffer without counting it
again (crates/format/src/unpack.rs). Entries::bytes refused such a
file once the repeats passed the size of the data after the JSON. It
now returns Arc<[u8]>, caches each range, and counts it once.
Overlapping ranges that differ are still copied and counted each time.
* vectorcraft-format: a refused entry does not count against the copy limit
Entries::bytes added a refused entry's length to the bytes copied, so a later entry that fit was refused too. It now counts an entry only once it is read.
* vectorcraft-format: Contents reads the document without keeping the preview and PDF
File<D> checks the shape of generator, preview and pdf by holding each
value in memory, and keeps the preview and PDF base64 it reads. Contents<D>
reads format, version, profiles, document and images, and skips the other
three as the JSON is read. It accepts and refuses the same files as File<D>.
* vectorcraft-format: Contents reads a repeated generator, preview or pdf
VectorCraft opens a file whose generator, preview or pdf member is repeated, since its reader does not declare those members, and refuses a repeated format, version, profiles, document or images. Contents now leaves the three members undeclared, so serde skips every copy of them, and still refuses the other repeated members. File and Head are unchanged.
* vectorcraft-format: sniff decodes JSON escapes in the format member
serde_json reads a file whose format key or value is written with escapes, such as \u0066ormat, as a VectorCraft file, and sniff compared the raw bytes and refused it. sniff now decodes the key and the value before comparing them.
* vectorcraft-format: File::read and Contents::read check the version
File::read and Contents::read are the readers of a file's JSON. Both
refuse another format or a version newer than VERSION after the JSON
reads, with ReadError::Envelope holding the Error (Format or TooNew), and
return ReadError::Json otherwise. File and Contents no longer implement
Deserialize, so a reader cannot skip the check.
Both read one implementation and treat repeated members the same way:
a repeated generator, preview or pdf is accepted, and File keeps the last
value, as serde_json reads a repeated key into a Value. A repeated
format, version, document, images or profiles is refused. A top-level
array holds format, version, document, images and profiles in that
order, as VectorCraft reads it; Contents' fields follow that order.
- Image::bytes returns None for an entry of more than MAX_ENTRY (1 GiB,
VectorCraft's limit on one entry of a compressed file), and
Image::bytes_at_most takes another limit. Entries keeps no per-entry
limit, as VectorCraft opens larger entries in uncompressed files.
- Entries::blob reads a range; Entries returns Arc<Vec<u8>>.
- marker_at finds BLOB_MAGIC where the writer puts it in a file's first
bytes; split_tail uses it.
- File, Contents, Head, Image, Part and ReadError are #[non_exhaustive].
* Add craft-ecproj-format: the envelope of EffectCraft project files
An .ecproj file is one JSON object: savedBy, then the saved view's
members, then the project's members, written with two-space indentation.
The crate defines that envelope with the view and the project as type
parameters, so the project model stays in EffectCraft:
- SCHEMA_VERSION (2) and check_schema, which refuses a newer schema
with EffectCraft's own message.
- File { saved_by, view, project }, serialized with both parts
flattened, and File::to_json.
- Head, which reads savedBy and schema from a JSON object, and
saved_by, which reads savedBy alone.
The tests write synthetic projects and compare the text with JSON
written by hand in the shape EffectCraft's writer gives. The workflow
runs fmt, clippy, the tests and a wasm32-unknown-unknown check.
* ci: do not keep the checkout token in the ecproj-format workflow
The checkout step sets persist-credentials: false, as jpeg.yml and media.yml do. No later step in the job uses the token.
* ecproj-format: write compact files, read savedBy as EffectCraft opens it
EffectCraft's Save Compact Project Files setting (effectcraft 07639b5f)
writes .ecproj files on one line with serde_json::to_string.
File::to_json_compact writes the same text, and the docs say the crate
writes and reads both forms.
Head now reads every file EffectCraft's Project::from_json opens: a
savedBy that is not a string, or appears more than once, reads as None,
and a project written as a JSON array reads with its first element as
the schema. saved_by also gives None for a repeated savedBy, as
EffectCraft's saved_by does. File's docs say the crate does not check
values and that serde_json writes NaN and infinity as null.
* ecproj-format: skip a non-string savedBy without keeping it
Head::read and saved_by read savedBy into a serde_json::Value before
dropping any value that is not a string, so a wide or deeply nested
savedBy was held in memory, and one nested past serde_json's recursion
limit made Head::read fail. A non-string savedBy, and every repeated
savedBy, is now skipped element by element with IgnoredAny, as other
members are. It still reads as None.
* ecproj-format: Head::read refuses a newer schema and reads objects only
Head::read is the one reader of a project file's envelope. It returns
ReadError: Json when the text is not one JSON object with a u32 schema
(the array form included), or TooNew when the schema is newer than
SCHEMA_VERSION, holding the file's savedBy so a message can name the
version that wrote it. TooNew reads as before. saved_by() is removed,
and Head no longer implements Deserialize.
TooNew, ReadError and ReadError::TooNew are #[non_exhaustive].
Re-review of craft-libs#482:
- en-GB and en-CA never fall back to an en_US Hunspell dictionary, which marked colour,
organise, centre… (CI's runner has one); without their own they use the built-in Moby list,
which has both spellings. en-US looks for en_US/en-US only.
- hunspell::candidates goes name by name across every folder, so a system en_GB beats an
en_US in the dictionaries folder.
- Built-in suggestions rank swaps, then common words, neighbouring keys, the same first letter;
no one-letter suggestions but a and i (hte -> the, recieve -> receive, wprd -> word).
- The generator leaves out Moby entries that are far more often typos: teh, abd, nad, ur, u, r
(wont and cant stay: words). en-words.dic regenerated (343,988 bytes).
- A load that started before a reload no longer notes its load error; reload clears the
English load errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Review of craft-libs#482:
- craft_proof_data is set only when every Language data file is embedded, and the new
craft_proof_no_data only when none is, so a partial folder (en-us but no pl) runs neither
the pattern tests nor the no-data tests.
- A CRAFT_I18N_DIR without language-data/ is watched (rerun-if-changed), so the build
notices when the checkout gains it.
- A CRAFT_I18N_DIR that isn't valid UTF-8 is reported and embeds nothing, instead of a lossy
path in include_str!.
- proof.yml checks out craft-i18n at 062a6375c322c88b96691a17f7b5fed71c51f7a8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Review of craft-libs#482:
- The built-in fallback is now the full public-domain Moby word list (data/en-words.dic,
words only, 343,992 bytes, from craft-i18n's en-us.dic by examples/en_words.rs) plus
WordCraft's own en-extra.txt (MIT OR Apache-2.0). An unknown word is misspelled; the
near-miss scan (which flagged fiber, theater, organize… and took 14 s on a 2,000-word
token) is gone. Suggestions are the words one edit away, looked up, for words of up to 32
letters. spelling_scales_over_one_long_token runs without the data, with a time bound.
- English Hunspell dictionaries ignore compounding (COMPOUNDMIN, COMPOUNDRULE…; their
ONLYINCOMPOUND stems are left out) and OCONV instead of refusing the file, and a file that
can't be read is skipped for the next name or folder.
- A load that started before english::reload() isn't kept (reloads counter, as slot.rs);
Englishes that use the same .dic share one copy. en-GB/en-CA falling back to en_US is
documented.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Ported from storytold/craft-libs#31 (9780561, "craft-proof: search the app's
own dictionaries folder on macOS and Linux too"), keeping WordCraft's API
(`hunspell::search_dirs`, `find`, `set_dictionary_dir`) instead of
`Search::for_app`.
The per-app folder was searched only on Windows. macOS now searches
~/Library/Application Support/WordCraft/dictionaries and Linux and the BSDs
$XDG_DATA_HOME/wordcraft/dictionaries (default ~/.local/share; lower-case, as
XDG folders and WordCraft's ~/.config/wordcraft are), after the setting and
WORDCRAFT_DICTIONARIES and before the shared system locations, so an app
started from Finder, the Dock or a launcher finds a dropped-in dictionary
without a setting (storytold/wordcraft#1063). Windows is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Merges the crates/proof history from storytold/wordcraft a88e00525..9c72ddf56
(storytold/wordcraft#5000, storytold/wordcraft#4235: 21 commits and 5 merges),
split with the same git filter-repo run as the first import, so the shared
history has the same commit ids and every commit keeps its author.
Conflicts in src/lib.rs, resolved with the CP1.2 changes kept: the embedded
`data` module beside the new `affix_suggest`; a dictionaries-folder change
reloads every language slot and English; English load errors still come from
the English source; the dictionaries-folder test helpers stay as CP1.2 split
them, with DICTIONARY_TESTS documented for the user-dictionary tests too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Tests and notes for rules that storytold/wordcraft#4129, storytold/wordcraft#4113, storytold/wordcraft#4109 and storytold/wordcraft#4101 added and
left uncovered:
- A border line at the top of a story with formatting left out of the
Track Changes markup is not a formatting change, and Reject All keeps
it.
- The author's own earlier formatting change on that paragraph stays the
one change.
- Reject All keeps the table of a "+---+" line.
- A border line that a content control's lock stops Backspace from
deleting stays as it is, and Enter is refused, untracked too.
- With tab indents on, Tab in a table cell moves to the next cell; with
them off, Backspace before an indented paragraph joins it.
- AutoCorrect and the spelling check's web and e-mail addresses after a
narrow no-break space and a figure space.
docs/typing-parity.md describes each, and lists a no-break space typed
after a word, which does not finish the word for AutoCorrect, as a
known gap.
- No embedded Moby list: English (en-US, en-GB, en-CA) uses an English
Hunspell .aff/.dic from the dictionaries folder or the system (the run's
English first, then another English), else data/en-core.txt.
- en-core.txt: 11,367 public-domain Moby Hyphenator II words (Grady Ward),
ranked by lower-case frequency in 50 public-domain Gutenberg books
(examples/en_core.rs, book ids recorded). 94 KB. Marks only near misses
of common words (swapped letters; one edit for lower-case words of 4+).
- Unreadable English dictionary (encoding, not UTF-8, no words, size):
error via dictionary_load_error, falls through to the core list; a bad
embedded list is reported the same way. Never a panic.
- Non-English text is never checked against English.
- New: english_source(lang) -> Option<EnglishSource>.
- Embedded-data path unchanged; tests with data unchanged.
- Tests: core list flags/accepts, folder Hunspell preferred over core list,
unreadable dictionaries reported, no_data English now marks misspellings;
two data-ignored tests now run without data. README, ATTRIBUTION, NOTICE.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
`Kmm` with `km/K` suggested `Km`, which the checker flags; `Km` itself got no
suggestion. A suggestion whose dictionary form is only correct through a
KEEPCASE stem now keeps that case (`km`). Other stems are re-cased as before
(`MIXCASX` → `MIXCASE`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
- Package renamed wordcraft-proof -> craft-proof; [workspace.dependencies]
entry; public API unchanged.
- build.rs embeds craft-i18n language-data/ (en-us, de, pl, pt, it, ru)
instead of include_str!/include_bytes! of WordCraft's assets/: warns when
CRAFT_I18N_DIR is unset, errors under CRAFT_I18N_REQUIRED=1.
- No data: English spelling marks and suggests nothing, hyphenation that
needs data is off, grammar and rule-based hyphenation work. no_data.rs runs
every public entry point without data; data-pinning tests are ignored then
(cfg craft_proof_data).
- es timing bound 75 -> 150 ms: craft-libs tests run at opt-level 0.
- proof.yml (fmt, clippy -D warnings, tests with and without data, release
guard, wasm32), README, CODEOWNERS for crates/proof.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
Merges the history of storytold/wordcraft crates/proof (84 commits, up to
wordcraft@a88e00525), split with git filter-repo so blame survives. Issue
references in the imported messages are rewritten to storytold/wordcraft#N.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
* krilla: CMYK, gray and spot colors keep their exact inks (patch 17)
Gray, CMYK, Separation and DeviceN colors stored each component as a
u8, so every ink was written as a multiple of 1/255: C75 became
0.7490196 and C4 M4 Y4 K4 became 0.039215688.
- cmyk, luma, separation and device_n Color::new_f32 take components
from 0 to 1 and write them as given. The u8 constructors are
unchanged.
- Gradient stops rewritten into one DeviceN, Separation or CMYK space
keep their components.
- Mesh gradients write corner colors at 16 bits a component.
* Tests: the 75% K mesh corner comment gives the asserted 0.749996
Dict validates a dictionary by skipping its values, and the skip accepts
some values that the parser cannot read: a name with a broken # escape
(/#zz), a number such as 12-.5, and a dictionary followed by stream with
no endstream. Dict::entries and Dict's Debug output unwrapped the result
of parsing every value, so either one panicked on such a dictionary.
Port LaurenzV/hayro#1501 to the vendored copy: entries skips an entry
whose value does not parse, and Debug prints the entries. ArrayIter::next
already ends the iteration on such an item here. Add LaurenzV/hayro#1501's
two tests, update the local hashes of dict.rs and array.rs and the repair
list in SOURCE.json, and note the change in vendor/README.craft-reader.md.
The first time a process opens a PrintTicket provider for a queue whose
provider is PrintConfig.dll, winspool.drv creates a process-wide cache
without a lock. When two threads make those first calls together,
winspool can keep a pointer to a cache it has already freed, and a later
PTOpenProvider call crashes the process with an access violation in
ntdll. Listing printers from several threads at once reaches it through
two_sided.
Hold one process-wide lock around every call that reaches the provider:
PrintCapabilities::fetch in two_sided and driver_ticket, the
PrintTicketBuilder in driver_ticket, fetch_xml_for_ticket in
imageable_margin, and the start of print_with_progress, where winprint
converts the ticket to a DEVMODE. The job releases the lock at its first
progress report, before it sends progress to the submitting thread. A
lock poisoned by a panic is still taken.
When the entropy stream ends before the frame does, the stream-end rule gives the
remaining samples their predictor values. That was silent: a raw-data record cut
short inside an intact CIFF heap decoded as a smeared image instead of an error.
`decode` now counts those samples (`Image::stream_end_fill`) and returns
`Error::Corrupt` when there are more than `Limits::max_stream_end_fill` (default
64, one block). `Limits` is non-exhaustive, so the field is additive. The frozen
format is unchanged: a fill within the limit decodes exactly as before.
Every real file measured fills none: 1,318 files of all seven verified bodies
(1,308 EOS D60), whose last samples come from an end-of-block code. The CC0 sample
test still decodes all ten files exactly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
craft_keymap::Key gains IntlBackslash, the key between left Shift and Z
on ISO keyboards (`<` and `>` on Turkish Q). Key::from_egui and to_egui
map it to egui's IntlBackslash, so the shortcut recorder records it and
the dispatcher runs its bindings. Its shortcut text is `IntlBackslash`
(`Cmd+IntlBackslash`, `Shift+IntlBackslash`), read without regard to
case; `<` and `>` still parse as no key.
The key counts as a symbol position: Shift stays in the chord, and once
the layout has typed `<` there, display_for shows `Ctrl+<`.
MAX_CHORD_TEXT goes from 64 to 72 bytes so that the longest canonical
trigger, `Cmd+Ctrl+Alt+Shift+IntlBackslash` pressed twice, reads back.
get_api shared the 4 MiB limit for small answers. A launcher version list of
100 releases can pass that: SolidCraft's was 6.5 MB (craft-launcher#1021).
Other small requests keep 4 MiB.
A Flatpak launcher wrote versions.json inside its own sandbox, and no Flatpak
app could read the launcher's folder, so pins never reached Flatpak apps, nor
host apps started from their menu entry. The file now sits in its own folder,
ArtCraft/versions under the host's per-user data folder, found from inside a
Flatpak too (HOST_XDG_DATA_HOME, else ~/.local/share). A Flatpak app reads it
with one read-only grant for that folder alone (pins::FLATPAK_GRANT).
folders::host_data_local_dir replaces install's private copy of the same rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>