Skip to main content

tilewright_pack/
read.rs

1// Copyright 2026 Seraphina Studios
2// SPDX-License-Identifier: MIT
3
4//! Reading and verifying packs. A pack is untrusted input: it is opened
5//! read-only and immutable, in SQLite's defensive mode, and its schema must
6//! match this version exactly before any other query runs. No value is
7//! loaded before SQL has checked its type and size, and verification streams
8//! rows instead of collecting whole tables.
9
10use crate::{
11    APPLICATION_ID, Error, ErrorCode, FORMAT, META_KEYS, SCHEMA_TABLES, USER_VERSION,
12    chunk::{
13        CHUNK_ENCODING, ChunkData, DecodeError, FieldRecord, MAXIMUM_CHUNK_BYTES,
14        MAXIMUM_RECORD_BYTES, ObjectRecord, PortalRecord, Visit, decode_chunk_with,
15    },
16    is_field_name, is_identifier, is_namespace,
17};
18use rusqlite::{
19    Connection, OpenFlags, OptionalExtension, Statement, config::DbConfig, params, types,
20};
21use sha2::{Digest, Sha256};
22use std::{
23    collections::{BTreeMap, BTreeSet},
24    fmt::Write as _,
25    io::Read as _,
26    path::{Path, PathBuf},
27};
28use tilewright_jsonc::{Document, Limits, MAX_SAFE_INTEGER, Value};
29use tilewright_source::{
30    CHUNK_SIZE, MAXIMUM_GROUND_LAYERS, MAXIMUM_LAYER_HEIGHT, Stairwell, StairwellDirection,
31    StairwellStep,
32};
33
34mod identities;
35mod rows;
36mod scan;
37pub use identities::Identity;
38pub use rows::{Anchor, Definition, Page, Plane, Zone};
39
40/// The page size of every pack.
41const PAGE_SIZE: i64 = 4096;
42
43/// Largest `meta` value other than `profile`.
44const MAXIMUM_META_BYTES: usize = 1024;
45
46/// Largest `sqlite_schema` value loaded; every expected one is shorter.
47const MAXIMUM_SCHEMA_BYTES: usize = 1024;
48
49/// The largest `object` index an `object_uids` row may hold. Every object
50/// takes more than one byte of a record of at most 16 MiB, so every index
51/// is below this.
52pub const MAXIMUM_OBJECT_INDEX: usize = MAXIMUM_RECORD_BYTES - 1;
53
54/// The default [`VerifyLimits::maximum_decoded_bytes`]: 8 GiB.
55pub const DEFAULT_MAXIMUM_DECODED_BYTES: u64 = 8 * 1024 * 1024 * 1024;
56
57/// The default [`VerifyLimits::maximum_pack_bytes`]: 8 GiB.
58pub const DEFAULT_MAXIMUM_PACK_BYTES: u64 = 8 * 1024 * 1024 * 1024;
59
60/// Bounds on the work opening and verifying a pack may do.
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub struct VerifyLimits {
63    /// Most bytes of chunk records decoded, summed over every chunk.
64    /// Verification fails once the chunks' recorded sizes pass it, before
65    /// the chunk that would pass it is decompressed.
66    pub maximum_decoded_bytes: u64,
67    /// Longest pack file opened or hashed. [`Pack::open_with`] checks the
68    /// file's length before SQLite opens it, verification checks the pages
69    /// again before its integrity check, and hashing fails once it has read
70    /// more, even from a file that has grown since it was opened.
71    /// Verification uses the lower of this and the limit the pack was
72    /// opened with.
73    pub maximum_pack_bytes: u64,
74}
75
76impl Default for VerifyLimits {
77    fn default() -> Self {
78        Self {
79            maximum_decoded_bytes: DEFAULT_MAXIMUM_DECODED_BYTES,
80            maximum_pack_bytes: DEFAULT_MAXIMUM_PACK_BYTES,
81        }
82    }
83}
84
85/// The error for a pack longer than `maximum` bytes.
86fn too_long(maximum: u64) -> Error {
87    Error::Rejected(
88        ErrorCode::TooLarge,
89        format!("the pack is more than {maximum} bytes, the pack size limit"),
90    )
91}
92
93/// Row counts of a pack.
94#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
95pub struct Summary {
96    pub strings: u64,
97    pub planes: u64,
98    pub zones: u64,
99    pub anchors: u64,
100    pub stairwells: u64,
101    pub chunks: u64,
102    pub definitions: u64,
103    /// Objects in every chunk, inventories included; counted by
104    /// [`Pack::verify`] only.
105    pub objects: u64,
106    /// Rows of `object_uids`: the objects that have a uid.
107    pub object_uids: u64,
108}
109
110/// Where the object with a uid is: its chunk, and its index in the chunk
111/// record's depth-first order (see [`ChunkData::object`]).
112#[derive(Clone, Debug, Eq, PartialEq)]
113pub struct UidLocation {
114    pub plane: String,
115    pub layer: i32,
116    pub cx: i64,
117    pub cy: i64,
118    pub index: usize,
119}
120
121/// The stairwell step on a tile.
122#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct StairwellTile {
124    /// The stairwell's ID.
125    pub stairwell: String,
126    /// The step's position in the stairwell's steps.
127    pub step: usize,
128    pub proto: String,
129}
130
131pub struct Pack {
132    connection: Connection,
133    source: Source,
134    /// The pack size limit it was opened with, which bounds
135    /// [`Pack::file_digest`].
136    maximum_bytes: u64,
137}
138
139/// Where a pack's bytes are, which decides what [`Pack::file_digest`]
140/// hashes and what verification compares the digest with.
141enum Source {
142    /// A file, hashed from its path; its name is the expected digest when
143    /// it is 64 hex digits.
144    File(PathBuf),
145    /// An image in memory: the SHA-256 of SQLite's read-only copy of it,
146    /// taken as the copy was filled because `rusqlite` serializes only
147    /// through `&mut Connection`, and the digest the caller expects, if any.
148    Image {
149        digest: [u8; 32],
150        expected: Option<[u8; 32]>,
151    },
152    /// A pack the compiler is checking in memory.
153    Compiled,
154}
155
156/// Percent-encodes a path for an SQLite URI. On Unix the path's bytes are
157/// encoded as they are, so paths that are not UTF-8 open too.
158#[cfg_attr(
159    unix,
160    expect(
161        clippy::unnecessary_wraps,
162        reason = "only platforms whose paths must be UTF-8 can fail"
163    )
164)]
165fn uri(absolute: &Path) -> Result<String, Error> {
166    #[cfg(unix)]
167    let bytes = std::os::unix::ffi::OsStrExt::as_bytes(absolute.as_os_str());
168    #[cfg(not(unix))]
169    let bytes = absolute
170        .to_str()
171        .ok_or_else(|| Error::Rejected(ErrorCode::Io, "pack path is not UTF-8".into()))?
172        .as_bytes();
173    // `file://` and then the absolute path, so a path that itself begins with
174    // `//` stays part of the file name instead of becoming a URI authority.
175    #[cfg(unix)]
176    let mut encoded = String::from("file://");
177    #[cfg(not(unix))]
178    let mut encoded = String::from("file:");
179    for byte in bytes {
180        if byte.is_ascii_alphanumeric() || b"/-_.~".contains(byte) {
181            encoded.push(char::from(*byte));
182        } else {
183            let _ = write!(encoded, "%{byte:02X}");
184        }
185    }
186    encoded.push_str("?immutable=1");
187    Ok(encoded)
188}
189
190/// A size bound as an SQL parameter.
191fn bound(bytes: usize) -> i64 {
192    i64::try_from(bytes).unwrap_or(i64::MAX)
193}
194
195/// The largest stored document: the source format's input limit.
196fn maximum_document_bytes() -> i64 {
197    bound(Limits::default().max_input_bytes)
198}
199
200/// The largest interned string: the source format's string limit.
201fn maximum_string_bytes() -> i64 {
202    bound(Limits::default().max_string_bytes)
203}
204
205/// SQL that is true when `column` is not an identifier
206/// (`^[a-z0-9][a-z0-9_.-]{0,127}$`). The length is checked first, without
207/// loading the value.
208fn not_identifier(column: &str) -> String {
209    // GLOB stops at a NUL byte, so NUL is rejected separately.
210    format!(
211        "(typeof({column}) != 'text' OR octet_length({column}) NOT BETWEEN 1 AND 128 \
212         OR instr({column}, char(0)) != 0 \
213         OR {column} NOT GLOB '[a-z0-9]*' OR {column} GLOB '*[^a-z0-9_.-]*')"
214    )
215}
216
217/// SQL for a `chunks c` row's data: the blob when the row's encoding is this
218/// one and the blob is within [`MAXIMUM_CHUNK_BYTES`], else 0 for another
219/// encoding and 1 for other data. Nothing larger is loaded.
220fn chunk_data_sql() -> String {
221    format!(
222        "CASE WHEN octet_length(c.encoding) = {length} AND c.encoding = '{CHUNK_ENCODING}' \
223         THEN CASE WHEN typeof(c.data) = 'blob' AND octet_length(c.data) <= {MAXIMUM_CHUNK_BYTES} \
224         THEN c.data ELSE 1 END ELSE 0 END",
225        length = CHUNK_ENCODING.len()
226    )
227}
228
229/// The decoded size a chunk's zstd frame records, or 0 when it has none
230/// (such a chunk fails to decode).
231fn recorded_size(value: &types::Value) -> u64 {
232    match value {
233        types::Value::Blob(data) => zstd::zstd_safe::get_frame_content_size(data)
234            .ok()
235            .flatten()
236            .unwrap_or(0),
237        _ => 0,
238    }
239}
240
241/// Decodes the value of [`chunk_data_sql`].
242fn chunk_data(value: &types::Value) -> Result<ChunkData, Error> {
243    chunk_data_with(value, None)
244}
245
246/// [`chunk_data`], streaming objects to `visit` when it is given.
247fn chunk_data_with(value: &types::Value, visit: Option<Visit<'_>>) -> Result<ChunkData, Error> {
248    match value {
249        types::Value::Blob(data) => decode_chunk_with(data, visit).map_err(Error::Chunk),
250        types::Value::Integer(0) => Err(Error::Rejected(
251            ErrorCode::ChunkEncoding,
252            format!("a chunk's encoding is not {CHUNK_ENCODING}"),
253        )),
254        _ => Err(Error::Rejected(
255            ErrorCode::ChunkData,
256            format!("a chunk's data is not a blob of at most {MAXIMUM_CHUNK_BYTES} bytes"),
257        )),
258    }
259}
260
261/// SQL for `column` when it is text the length of an identifier, at most
262/// 128 bytes, else NULL; callers then check the pattern.
263fn bounded_identifier(column: &str) -> String {
264    format!(
265        "CASE WHEN typeof({column}) = 'text' AND octet_length({column}) BETWEEN 1 AND 128 THEN {column} END"
266    )
267}
268
269/// SQL for `column` when it is an index a source array can have, from 0 to
270/// one less than the array limit, else NULL.
271fn step_index(column: &str) -> String {
272    format!(
273        "CASE WHEN typeof({column}) = 'integer' AND {column} BETWEEN 0 AND {} THEN {column} END",
274        Limits::default().max_array_items - 1
275    )
276}
277
278/// SQL for `column` when it is an integer from `minimum` to `maximum`, else
279/// NULL.
280fn bounded_integer(column: &str, minimum: i64, maximum: i64) -> String {
281    format!(
282        "CASE WHEN typeof({column}) = 'integer' AND {column} BETWEEN {minimum} AND {maximum} THEN {column} END"
283    )
284}
285
286/// The chunk coordinates of format 1, whose 32-tile chunks keep every tile
287/// within the source format's integer range.
288const CHUNKS: (i64, i64) = (
289    -(MAX_SAFE_INTEGER / CHUNK_SIZE as i64),
290    (MAX_SAFE_INTEGER - (CHUNK_SIZE as i64 - 1)) / CHUNK_SIZE as i64,
291);
292
293/// The columns of an `object_uids` row, each NULL when out of bounds.
294type StoredLocation = (
295    Option<String>,
296    Option<i64>,
297    Option<i64>,
298    Option<i64>,
299    Option<i64>,
300);
301
302/// The error for an `object_uids` row that is not one a compiler writes.
303fn bad_uid_row() -> Error {
304    Error::Rejected(
305        ErrorCode::ObjectUid,
306        "an object_uids row's plane is not an identifier, or its layer, chunk or index is out of range".into(),
307    )
308}
309
310/// SQL for `column` when it is text of at most `?1` bytes, else NULL.
311fn bounded_text(column: &str) -> String {
312    format!(
313        "CASE WHEN typeof({column}) = 'text' AND octet_length({column}) <= ?1 THEN {column} END"
314    )
315}
316
317/// Parses a stored document, which must be canonical JSON of an object;
318/// `code` is the rule a bad one breaks.
319fn stored_document(text: Option<String>, what: &str, code: ErrorCode) -> Result<Document, Error> {
320    let invalid = || {
321        Error::Rejected(
322            code,
323            format!("{what} is not canonical JSON of an object with namespaced custom data"),
324        )
325    };
326    let text = text.ok_or_else(invalid)?;
327    if !scan::compact(&text) {
328        return Err(invalid());
329    }
330    let document = Document::parse(text, &crate::canonical_limits()).map_err(|_| invalid())?;
331    // `custom` data, where a document has it, is keyed by namespaces as in
332    // the source format.
333    let custom_ok = match document.root().get("custom") {
334        None => true,
335        Some(custom) => custom
336            .as_object()
337            .is_some_and(|members| members.iter().all(|member| is_namespace(&member.key))),
338    };
339    if document.root().as_object().is_none()
340        || document.canonical(document.root()) != document.source()
341        || !custom_ok
342    {
343        return Err(invalid());
344    }
345    Ok(document)
346}
347
348/// Whether a stored profile's `projection` is one a source profile can
349/// have: `isometric` with `tile_width` and `tile_height`, or `top_down` with
350/// `tile_size`, each an integer from 1 to `u32::MAX`, and no other keys.
351fn projection_ok(profile: &Document) -> bool {
352    let Some(members) = profile.root().get("projection").and_then(Value::as_object) else {
353        return false;
354    };
355    let keys: BTreeSet<&str> = members.iter().map(|member| member.key.as_str()).collect();
356    let sizes: &[&str] = match members
357        .iter()
358        .find(|member| member.key == "kind")
359        .and_then(|member| member.value.as_str())
360    {
361        Some("isometric") => &["tile_width", "tile_height"],
362        Some("top_down") => &["tile_size"],
363        _ => return false,
364    };
365    keys.len() == sizes.len() + 1
366        && sizes.iter().all(|size| {
367            members
368                .iter()
369                .find(|member| member.key == *size)
370                .and_then(|member| member.value.as_i64())
371                .is_some_and(|value| (1..=i64::from(u32::MAX)).contains(&value))
372        })
373}
374
375/// The `layer_height` of a stored profile: `Ok(None)` when it has none, and
376/// an error unless it is an integer from 1 to [`MAXIMUM_LAYER_HEIGHT`].
377fn stored_layer_height(profile: &Document) -> Result<Option<u32>, Error> {
378    profile
379        .root()
380        .get("layer_height")
381        .map(|value| {
382            value
383                .as_i64()
384                .filter(|height| (1..=i64::from(MAXIMUM_LAYER_HEIGHT)).contains(height))
385                .and_then(|height| u32::try_from(height).ok())
386                .ok_or_else(|| {
387                    Error::Rejected(
388                        ErrorCode::Profile,
389                        format!(
390                            "the stored profile's layer_height is not an integer from 1 to {MAXIMUM_LAYER_HEIGHT}"
391                        ),
392                    )
393                })
394        })
395        .transpose()
396}
397
398/// Reads a stored stairwell document, already checked to be canonical JSON
399/// of an object with namespaced custom data, into a stairwell with the
400/// source format's shape: an identifier, at least two connected steps,
401/// each a 32-bit layer, a position within the integer range and a
402/// prototype identifier, and a valid direction and flag.
403fn stored_stairwell(document: &Document) -> Result<Stairwell, Error> {
404    let invalid = || {
405        Error::Rejected(
406            ErrorCode::Stairwell,
407            "a stairwell document does not have the source format's shape".into(),
408        )
409    };
410    let closed = |value: &Value, keys: &[&str]| {
411        value.as_object().is_some_and(|members| {
412            members
413                .iter()
414                .all(|member| keys.contains(&member.key.as_str()))
415        })
416    };
417    let root = document.root();
418    if !closed(
419        root,
420        &[
421            "id",
422            "steps",
423            "direction",
424            "hidden_on_other_layers",
425            "custom",
426        ],
427    ) {
428        return Err(invalid());
429    }
430    let id = text(document, "id")
431        .filter(|id| is_identifier(id))
432        .ok_or_else(invalid)?;
433    let direction = match root.get("direction") {
434        None => StairwellDirection::Both,
435        Some(value) => value
436            .as_str()
437            .and_then(StairwellDirection::parse)
438            .ok_or_else(invalid)?,
439    };
440    let hidden_on_other_layers = match root.get("hidden_on_other_layers") {
441        None => true,
442        Some(value) => value.as_bool().ok_or_else(invalid)?,
443    };
444    let in_range = |value: &Value| {
445        value
446            .as_i64()
447            .filter(|number| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(number))
448    };
449    let mut steps = Vec::new();
450    for step in root
451        .get("steps")
452        .and_then(Value::as_array)
453        .ok_or_else(invalid)?
454    {
455        if !closed(step, &["layer", "at", "proto"]) {
456            return Err(invalid());
457        }
458        let layer = step
459            .get("layer")
460            .and_then(Value::as_i64)
461            .and_then(|layer| i32::try_from(layer).ok());
462        let at = match step.get("at").and_then(Value::as_array) {
463            Some([x, y]) => in_range(x).zip(in_range(y)).map(|(x, y)| [x, y]),
464            _ => None,
465        };
466        let proto = step
467            .get("proto")
468            .and_then(Value::as_str)
469            .filter(|proto| is_identifier(proto));
470        let (Some(layer), Some(at), Some(proto)) = (layer, at, proto) else {
471            return Err(invalid());
472        };
473        steps.push(StairwellStep {
474            layer,
475            at,
476            proto: proto.to_owned(),
477        });
478    }
479    if !Stairwell::connects(&steps) {
480        return Err(Error::Rejected(
481            ErrorCode::Stairwell,
482            format!(
483                "the steps of stairwell {id} are not on adjacent layers in one direction, at most one tile apart"
484            ),
485        ));
486    }
487    Ok(Stairwell {
488        id: id.to_owned(),
489        steps,
490        direction,
491        hidden_on_other_layers,
492        custom: root.get("custom").map(|custom| document.canonical(custom)),
493        canonical: document.source().to_owned(),
494    })
495}
496
497/// A string member of a document's root object.
498fn text<'d>(document: &'d Document, key: &str) -> Option<&'d str> {
499    document.root().get(key).and_then(Value::as_str)
500}
501
502/// Column `index` as text: `None` when it is not text, as a bounded column
503/// is when out of bounds. SQLite stores any bytes as text, so text that is
504/// not UTF-8 is `code`'s error rather than an SQLite one.
505fn utf8_column(
506    row: &rusqlite::Row<'_>,
507    index: usize,
508    code: ErrorCode,
509) -> Result<Option<String>, Error> {
510    match row.get_ref(index)? {
511        types::ValueRef::Text(bytes) => std::str::from_utf8(bytes)
512            .map(|text| Some(text.to_owned()))
513            .map_err(|_| Error::Rejected(code, "a stored value is not UTF-8 text".into())),
514        _ => Ok(None),
515    }
516}
517
518/// A stairwell from a row of `id, document`, bounded in SQL by
519/// [`bounded_identifier`] and [`bounded_text`] (NULL when out of bounds),
520/// after checking that the document has the row's ID. It fails with
521/// [`ErrorCode::DecodeBudget`] when the document lists more than
522/// `maximum_steps` steps, before it is parsed.
523pub(super) fn stairwell_row_within(
524    row: &rusqlite::Row<'_>,
525    maximum_steps: u64,
526) -> Result<Stairwell, Error> {
527    let id = utf8_column(row, 0, ErrorCode::Stairwell)?;
528    let text = utf8_column(row, 1, ErrorCode::Stairwell)?;
529    // The steps are counted before the document is parsed: a scan of the
530    // text, exact for the canonical JSON a stored document must be.
531    let steps = text
532        .as_deref()
533        .filter(|text| scan::compact(text))
534        .and_then(|text| scan::root_array_items(text, "steps"));
535    if steps.is_some_and(|steps| u64::try_from(steps).unwrap_or(u64::MAX) > maximum_steps) {
536        return Err(Error::Rejected(
537            ErrorCode::DecodeBudget,
538            format!("a stairwell lists more than {maximum_steps} steps, the read's budget"),
539        ));
540    }
541    let document = stored_document(text, "a stairwell document", ErrorCode::Stairwell)?;
542    let stairwell = stored_stairwell(&document)?;
543    if id.as_deref() != Some(stairwell.id.as_str()) {
544        return Err(Error::Rejected(
545            ErrorCode::Stairwell,
546            format!(
547                "the document of stairwell {} disagrees with its row",
548                stairwell.id
549            ),
550        ));
551    }
552    Ok(stairwell)
553}
554
555impl Pack {
556    /// Opens a pack with [`VerifyLimits::default`]; see [`Pack::open_with`].
557    ///
558    /// # Errors
559    /// Returns an error if the file is not a pack of this format or is
560    /// longer than the default pack size limit.
561    pub fn open(path: &Path) -> Result<Self, Error> {
562        Self::open_with(path, &VerifyLimits::default())
563    }
564
565    /// Opens a pack and checks its size, identity and schema. A file longer
566    /// than `limits.maximum_pack_bytes` is rejected before SQLite opens it,
567    /// and [`Pack::file_digest`] reads at most that many bytes.
568    ///
569    /// # Errors
570    /// Returns an error if the file is not a pack of this format or is
571    /// longer than the limit.
572    pub fn open_with(path: &Path, limits: &VerifyLimits) -> Result<Self, Error> {
573        // The same absolute path opens the database and is hashed later, so
574        // a change of working directory cannot point them at different files.
575        let path = std::path::absolute(path)?;
576        let metadata = std::fs::metadata(&path)?;
577        if !metadata.is_file() {
578            return Err(Error::Rejected(
579                ErrorCode::NotAFile,
580                format!("{} is not a file", path.display()),
581            ));
582        }
583        if metadata.len() > limits.maximum_pack_bytes {
584            return Err(too_long(limits.maximum_pack_bytes));
585        }
586        let mut head = Vec::with_capacity(HEADER_BYTES);
587        std::fs::File::open(&path)?
588            .take(HEADER_BYTES as u64)
589            .read_to_end(&mut head)?;
590        check_header(&head)?;
591        let connection = Connection::open_with_flags(
592            uri(&path)?,
593            OpenFlags::SQLITE_OPEN_READ_ONLY
594                | OpenFlags::SQLITE_OPEN_URI
595                | OpenFlags::SQLITE_OPEN_NO_MUTEX,
596        )?;
597        harden(&connection)?;
598        check_identity(&connection)?;
599        // The file SQLite opened may have replaced the one whose length was
600        // checked, so its pages are checked against the limit too.
601        check_pages(&connection, std::fs::metadata(&path)?.len(), limits)?;
602        Ok(Self {
603            connection,
604            source: Source::File(path),
605            maximum_bytes: limits.maximum_pack_bytes,
606        })
607    }
608
609    /// Opens a pack from its bytes, as [`Pack::open_from`] does.
610    ///
611    /// # Errors
612    /// Returns an error if the bytes are not a pack of this format or are
613    /// more than the pack size limit.
614    pub fn open_bytes(
615        bytes: &[u8],
616        digest: Option<[u8; 32]>,
617        limits: &VerifyLimits,
618    ) -> Result<Self, Error> {
619        let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
620        Self::open_from(bytes, length, digest, limits)
621    }
622
623    /// Opens a pack image of exactly `length` bytes, which `read` yields, and checks
624    /// its size, identity and schema as [`Pack::open_with`] checks a file's.
625    /// An image longer than `limits.maximum_pack_bytes` is rejected before
626    /// any of it is read. SQLite reads a read-only copy in memory, which
627    /// stays the pack's bytes until it is dropped. The copy is hashed as it
628    /// is filled, so opening an image costs one SHA-256 of it.
629    ///
630    /// `digest`, when given, is the SHA-256 the image must have. Like a file
631    /// name that is a digest, verification compares it with the image's
632    /// digest; opening does not compare them.
633    ///
634    /// # Errors
635    /// Returns an error if `read` fails, ends early or yields more than
636    /// `length` bytes, the image is longer than the limit or than memory can
637    /// hold, or it is not a pack of this format.
638    pub fn open_from(
639        read: impl std::io::Read,
640        length: u64,
641        digest: Option<[u8; 32]>,
642        limits: &VerifyLimits,
643    ) -> Result<Self, Error> {
644        if length > limits.maximum_pack_bytes {
645            return Err(too_long(limits.maximum_pack_bytes));
646        }
647        let size = usize::try_from(length).map_err(|_| {
648            Error::Io(std::io::Error::new(
649                std::io::ErrorKind::OutOfMemory,
650                "the pack is larger than memory can hold",
651            ))
652        })?;
653        let mut connection = Connection::open_in_memory_with_flags(
654            OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
655        )?;
656        let mut image = Filled {
657            inner: read,
658            missing: false,
659            failure: None,
660            head: Vec::with_capacity(HEADER_BYTES),
661            hasher: Sha256::new(),
662        };
663        // An empty image is an empty database, as an empty file is; SQLite
664        // cannot allocate a copy of no bytes.
665        if size > 0 {
666            // `Filled` never fails, so SQLite always takes ownership of its
667            // copy and frees it with the connection, even when the image
668            // could not be read.
669            connection.deserialize_read_exact(rusqlite::MAIN_DB, &mut image, size, true)?;
670        }
671        if let Some(error) = image.failure {
672            return Err(Error::Io(error));
673        }
674        if image.missing {
675            return Err(Error::Io(std::io::Error::new(
676                std::io::ErrorKind::UnexpectedEof,
677                format!("the pack image ends before its {length} bytes"),
678            )));
679        }
680        // The image is exactly `length` bytes: one that goes on is not the
681        // pack its length describes.
682        if read_some(&mut image.inner)? {
683            return Err(Error::Rejected(
684                ErrorCode::FileLength,
685                format!("the pack image is longer than {length} bytes"),
686            ));
687        }
688        // SQLite has not read the copy yet: it first does so in
689        // `check_identity`.
690        check_header(&image.head)?;
691        harden(&connection)?;
692        check_identity(&connection)?;
693        check_pages(&connection, length, limits)?;
694        Ok(Self {
695            connection,
696            source: Source::Image {
697                digest: image.hasher.finalize().into(),
698                expected: digest,
699            },
700            maximum_bytes: limits.maximum_pack_bytes,
701        })
702    }
703
704    /// A pack the compiler has just built, checked like an opened one.
705    pub(crate) fn compiled(connection: Connection) -> Result<Self, Error> {
706        check_identity(&connection)?;
707        Ok(Self {
708            connection,
709            source: Source::Compiled,
710            // There is no file to hash, so the limit is never used.
711            maximum_bytes: u64::MAX,
712        })
713    }
714
715    pub(crate) fn into_connection(self) -> Connection {
716        self.connection
717    }
718
719    /// A `meta` value.
720    ///
721    /// # Errors
722    /// Returns an error if SQLite fails.
723    pub fn meta(&self, key: &str) -> Result<Option<String>, Error> {
724        let mut statement = self
725            .connection
726            .prepare("SELECT value FROM meta WHERE key = ?1")?;
727        let mut rows = statement.query([key])?;
728        match rows.next()? {
729            Some(row) => utf8_column(row, 0, ErrorCode::Meta),
730            None => Ok(None),
731        }
732    }
733
734    /// An interned string.
735    ///
736    /// # Errors
737    /// Returns an error if SQLite fails or the stored value is not text of
738    /// at most 1 MiB.
739    pub fn string(&self, id: u32) -> Result<Option<String>, Error> {
740        let mut statement = self.connection.prepare(&format!(
741            "SELECT {} FROM strings WHERE id = ?2",
742            bounded_text("value")
743        ))?;
744        let mut rows = statement.query(params![maximum_string_bytes(), id])?;
745        let Some(row) = rows.next()? else {
746            return Ok(None);
747        };
748        match utf8_column(row, 0, ErrorCode::String)? {
749            Some(text) => Ok(Some(text)),
750            None => Err(Error::Rejected(
751                ErrorCode::String,
752                "a string is not text of at most 1 MiB".into(),
753            )),
754        }
755    }
756
757    /// Decodes one chunk into its full model; see [`decode_chunk`].
758    ///
759    /// [`decode_chunk`]: crate::decode_chunk
760    ///
761    /// # Errors
762    /// Returns an error if SQLite fails or the stored record is invalid.
763    pub fn chunk(
764        &self,
765        plane: &str,
766        layer: i32,
767        cx: i64,
768        cy: i64,
769    ) -> Result<Option<ChunkData>, Error> {
770        self.chunk_within(plane, layer, cx, cy, u64::MAX)
771    }
772
773    /// [`Pack::chunk`], with a budget of `maximum_entries` entries: its
774    /// objects, inventories included, and their fields. A record of at most
775    /// 16 MiB can hold about a million small objects, or as many fields, so
776    /// a caller that builds a value for each, such as a browser, can bound
777    /// them. Under a budget the record is first decoded keeping one object
778    /// at a time, and the read fails with [`ErrorCode::DecodeBudget`] once
779    /// it counts more, before any chunk is built.
780    ///
781    /// # Errors
782    /// Returns an error if SQLite fails, the stored record is invalid, or it
783    /// holds more entries than the budget.
784    pub fn chunk_within(
785        &self,
786        plane: &str,
787        layer: i32,
788        cx: i64,
789        cy: i64,
790        maximum_entries: u64,
791    ) -> Result<Option<ChunkData>, Error> {
792        let value: Option<types::Value> = self
793            .connection
794            .query_row(
795                &format!(
796                    "SELECT {} FROM chunks c WHERE c.plane = ?1 AND c.layer = ?2 AND c.cx = ?3 AND c.cy = ?4",
797                    chunk_data_sql()
798                ),
799                params![plane, layer, cx, cy],
800                |row| row.get(0),
801            )
802            .optional()?;
803        let Some(value) = value else {
804            return Ok(None);
805        };
806        if maximum_entries < u64::MAX {
807            let mut entries: u64 = 0;
808            let mut over = false;
809            let mut count = |object: &ObjectRecord| {
810                let fields = u64::try_from(object.fields.len()).unwrap_or(u64::MAX);
811                entries = entries.saturating_add(1).saturating_add(fields);
812                over = entries > maximum_entries;
813                if over {
814                    Err(DecodeError(
815                        "the chunk holds more entries than the budget".into(),
816                    ))
817                } else {
818                    Ok(())
819                }
820            };
821            let counted = chunk_data_with(&value, Some(&mut count));
822            if over {
823                return Err(Error::Rejected(
824                    ErrorCode::DecodeBudget,
825                    format!(
826                        "the chunk holds more than {maximum_entries} objects and fields, the read's budget"
827                    ),
828                ));
829            }
830            counted?;
831        }
832        chunk_data(&value).map(Some)
833    }
834
835    /// The profile's `layer_height`: height units per layer, or `None` when
836    /// the profile does not stack layers. The stored profile is checked as a
837    /// full verification checks it.
838    ///
839    /// # Errors
840    /// Returns an error if SQLite fails or the stored profile is invalid.
841    pub fn layer_height(&self) -> Result<Option<u32>, Error> {
842        Ok(self.profile_layers()?.2)
843    }
844
845    /// One stairwell of a plane, read from its stored document. Like
846    /// [`Pack::chunk`], it checks the shape and bounds of what it loads;
847    /// whether the plane and the steps' prototypes exist is checked by
848    /// [`Pack::verify`].
849    ///
850    /// # Errors
851    /// Returns an error if SQLite fails, or the stored stairwell does not
852    /// have the source format's shape or disagrees with its row.
853    pub fn stairwell(&self, plane: &str, id: &str) -> Result<Option<Stairwell>, Error> {
854        self.stairwell_within(plane, id, u64::MAX)
855    }
856
857    /// [`Pack::stairwell`], with a budget of `maximum_steps` steps. A stored
858    /// document can list about a quarter of a million, so a caller that
859    /// builds a value for each, such as a browser, can bound them; the read
860    /// fails with [`ErrorCode::DecodeBudget`] before it builds any step.
861    ///
862    /// # Errors
863    /// Returns an error if SQLite fails, the stored stairwell is invalid or
864    /// disagrees with its row, or it lists more steps than the budget.
865    pub fn stairwell_within(
866        &self,
867        plane: &str,
868        id: &str,
869        maximum_steps: u64,
870    ) -> Result<Option<Stairwell>, Error> {
871        let mut statement = self.connection.prepare(&format!(
872            "SELECT {}, {} FROM stairwells WHERE plane = ?2 AND id = ?3",
873            bounded_identifier("id"),
874            bounded_text("document"),
875        ))?;
876        let mut rows = statement.query(params![maximum_document_bytes(), plane, id])?;
877        rows.next()?
878            .map(|row| stairwell_row_within(row, maximum_steps))
879            .transpose()
880    }
881
882    /// The stairwell step on a tile of a plane, if any. It checks the shape
883    /// and bounds of what it loads; whether the stairwell and prototype
884    /// exist is checked by [`Pack::verify`].
885    ///
886    /// # Errors
887    /// Returns an error if SQLite fails or the stored step is invalid.
888    pub fn stairwell_step(
889        &self,
890        plane: &str,
891        layer: i32,
892        x: i64,
893        y: i64,
894    ) -> Result<Option<StairwellTile>, Error> {
895        let mut statement = self.connection.prepare(&format!(
896            "SELECT {}, {}, {} FROM stairwell_steps \
897             WHERE plane = ?1 AND layer = ?2 AND x = ?3 AND y = ?4",
898            bounded_identifier("stairwell"),
899            step_index("step"),
900            bounded_identifier("proto"),
901        ))?;
902        let mut rows = statement.query(params![plane, layer, x, y])?;
903        let Some(row) = rows.next()? else {
904            return Ok(None);
905        };
906        let found: (Option<String>, Option<i64>, Option<String>) = (
907            utf8_column(row, 0, ErrorCode::StairwellStep)?,
908            row.get(1)?,
909            utf8_column(row, 2, ErrorCode::StairwellStep)?,
910        );
911        match found {
912            (Some(stairwell), Some(step), Some(proto))
913                if is_identifier(&stairwell) && is_identifier(&proto) =>
914            {
915                Ok(Some(StairwellTile {
916                    stairwell,
917                    step: usize::try_from(step).map_err(|_| {
918                        Error::Rejected(
919                            ErrorCode::StairwellStep,
920                            "a stairwell step has a negative index".into(),
921                        )
922                    })?,
923                    proto,
924                }))
925            }
926            _ => Err(Error::Rejected(
927                ErrorCode::StairwellStep,
928                "a stairwell step's stairwell or proto is not an identifier, or its index is not one a source array can have".into(),
929            )),
930        }
931    }
932
933    /// Where the object with `uid` is, from its `object_uids` row. Like
934    /// [`Pack::chunk`], it checks the shape and bounds of what it loads;
935    /// whether the row agrees with the chunk records is checked by
936    /// [`Pack::verify`], or for this one object by [`Pack::object`].
937    ///
938    /// # Errors
939    /// Returns an error if SQLite fails or the stored row is invalid.
940    pub fn object_location(&self, uid: &str) -> Result<Option<UidLocation>, Error> {
941        if !is_identifier(uid) {
942            return Ok(None);
943        }
944        let mut statement = self.connection.prepare(&format!(
945            "SELECT {}, {}, {}, {}, {} FROM object_uids WHERE uid = ?1",
946            bounded_identifier("plane"),
947            bounded_integer("layer", i64::from(i32::MIN), i64::from(i32::MAX)),
948            bounded_integer("cx", CHUNKS.0, CHUNKS.1),
949            bounded_integer("cy", CHUNKS.0, CHUNKS.1),
950            bounded_integer("object", 0, bound(MAXIMUM_OBJECT_INDEX)),
951        ))?;
952        let mut rows = statement.query([uid])?;
953        let found: Option<StoredLocation> = match rows.next()? {
954            Some(row) => Some((
955                utf8_column(row, 0, ErrorCode::ObjectUid)?,
956                row.get(1)?,
957                row.get(2)?,
958                row.get(3)?,
959                row.get(4)?,
960            )),
961            None => None,
962        };
963        let Some(found) = found else {
964            return Ok(None);
965        };
966        match found {
967            (Some(plane), Some(layer), Some(cx), Some(cy), Some(index))
968                if is_identifier(&plane) =>
969            {
970                Ok(Some(UidLocation {
971                    plane,
972                    layer: i32::try_from(layer).map_err(|_| bad_uid_row())?,
973                    cx,
974                    cy,
975                    index: usize::try_from(index).map_err(|_| bad_uid_row())?,
976                }))
977            }
978            _ => Err(bad_uid_row()),
979        }
980    }
981
982    /// The object with `uid` and where it is. It decodes the object's chunk,
983    /// as [`Pack::chunk`] does, and fails unless the object at the row's
984    /// index has `uid`.
985    ///
986    /// # Errors
987    /// Returns an error if SQLite fails, the stored row or chunk is invalid,
988    /// or they disagree.
989    pub fn object(&self, uid: &str) -> Result<Option<(UidLocation, ObjectRecord)>, Error> {
990        self.object_within(uid, u64::MAX)
991    }
992
993    /// [`Pack::object`], decoding the object's chunk with a budget of
994    /// `maximum_entries` objects and fields; see [`Pack::chunk_within`].
995    ///
996    /// # Errors
997    /// Returns an error if SQLite fails, the stored row or chunk is invalid,
998    /// they disagree, or the chunk holds more entries than the budget.
999    pub fn object_within(
1000        &self,
1001        uid: &str,
1002        maximum_entries: u64,
1003    ) -> Result<Option<(UidLocation, ObjectRecord)>, Error> {
1004        let Some(location) = self.object_location(uid)? else {
1005            return Ok(None);
1006        };
1007        let object = self
1008            .chunk_within(
1009                &location.plane,
1010                location.layer,
1011                location.cx,
1012                location.cy,
1013                maximum_entries,
1014            )?
1015            .and_then(|chunk| {
1016                chunk
1017                    .object(location.index)
1018                    .filter(|object| object.uid.as_deref() == Some(uid))
1019                    .cloned()
1020            })
1021            .ok_or_else(|| {
1022                Error::Rejected(
1023                    ErrorCode::ObjectUid,
1024                    format!("the object_uids row of {uid} does not name an object with that uid"),
1025                )
1026            })?;
1027        Ok(Some((location, object)))
1028    }
1029
1030    /// Row counts.
1031    ///
1032    /// # Errors
1033    /// Returns an error if SQLite fails.
1034    pub fn summary(&self) -> Result<Summary, Error> {
1035        let count = |table: &str| -> Result<u64, Error> {
1036            let value: i64 =
1037                self.connection
1038                    .query_row(&format!("SELECT count(*) FROM {table}"), [], |row| {
1039                        row.get(0)
1040                    })?;
1041            Ok(u64::try_from(value).unwrap_or(0))
1042        };
1043        Ok(Summary {
1044            strings: count("strings")?,
1045            planes: count("planes")?,
1046            zones: count("zones")?,
1047            anchors: count("anchors")?,
1048            stairwells: count("stairwells")?,
1049            chunks: count("chunks")?,
1050            definitions: count("definitions")?,
1051            objects: 0,
1052            object_uids: count("object_uids")?,
1053        })
1054    }
1055
1056    /// The SHA-256 of the pack file, read from its path when called, or of
1057    /// the image a pack was opened from.
1058    ///
1059    /// For a file, this hashes whatever file is at the pack's path now, not
1060    /// necessarily the file SQLite opened: the digest binds to the path.
1061    /// Packs are content-addressed files opened from directories the caller
1062    /// controls; a directory that others can write to is not supported. It
1063    /// reads at most the pack size limit the pack was opened with, and fails
1064    /// for a longer file. An image's digest was taken as SQLite's read-only
1065    /// copy was filled, so it binds to the bytes the reads see.
1066    ///
1067    /// # Errors
1068    /// Returns an error if the file cannot be read or is longer than the
1069    /// limit.
1070    pub fn file_digest(&self) -> Result<[u8; 32], Error> {
1071        self.digest_within(self.maximum_bytes)
1072    }
1073
1074    /// [`Pack::file_digest`], reading at most `maximum` bytes.
1075    fn digest_within(&self, maximum: u64) -> Result<[u8; 32], Error> {
1076        let path = match &self.source {
1077            Source::File(path) => path,
1078            Source::Image { digest, .. } => return Ok(*digest),
1079            Source::Compiled => {
1080                return Err(Error::Rejected(
1081                    ErrorCode::NotAFile,
1082                    "the pack is not a file".into(),
1083                ));
1084            }
1085        };
1086        let file = std::fs::File::open(path)?;
1087        if file.metadata()?.len() > maximum {
1088            return Err(too_long(maximum));
1089        }
1090        // The length can change once checked, so the read stops one byte
1091        // past the limit.
1092        let mut file = file.take(maximum.saturating_add(1));
1093        let mut hasher = Sha256::new();
1094        let mut buffer = vec![0; 64 * 1024];
1095        let mut total: u64 = 0;
1096        loop {
1097            let read = file.read(&mut buffer)?;
1098            if read == 0 {
1099                break;
1100            }
1101            total = total.saturating_add(u64::try_from(read).unwrap_or(u64::MAX));
1102            if total > maximum {
1103                return Err(too_long(maximum));
1104            }
1105            hasher.update(&buffer[..read]);
1106        }
1107        Ok(hasher.finalize().into())
1108    }
1109
1110    /// Checks the whole pack with the default decoded-byte budget and the
1111    /// pack size limit the pack was opened with; see [`Pack::verify_with`].
1112    ///
1113    /// # Errors
1114    /// Returns the first problem found.
1115    pub fn verify(&self) -> Result<Summary, Error> {
1116        self.verify_with(&VerifyLimits {
1117            maximum_pack_bytes: self.maximum_bytes,
1118            ..VerifyLimits::default()
1119        })
1120    }
1121
1122    /// Checks the whole pack: SQLite integrity, the file name digest when the
1123    /// name is one, string, plane, zone, anchor and definition rows, every
1124    /// stored document, every chunk record, and every string, layer, ground
1125    /// count and portal target a record references.
1126    ///
1127    /// Decoding every chunk is linear in the pack's decoded size, which
1128    /// compression can make far larger than the file, so it stops and fails
1129    /// once `limits.maximum_decoded_bytes` would be passed. The integrity
1130    /// check and the hash are linear in the file's size, so the pages are
1131    /// checked against the pack size limit before either runs, and the hash
1132    /// reads at most that many bytes. The limit is the lower of
1133    /// `limits.maximum_pack_bytes` and the one the pack was opened with, so
1134    /// verifying never reads more than opening allowed. The file name check
1135    /// hashes the file at the pack's path, so it binds to the path; see
1136    /// [`Pack::file_digest`]. An image's digest is compared before the
1137    /// integrity check instead, so that SQLite's checks do not run on an
1138    /// image that is not the one the caller expects.
1139    ///
1140    /// # Errors
1141    /// Returns the first problem found, including an exceeded limit.
1142    pub fn verify_with(&self, limits: &VerifyLimits) -> Result<Summary, Error> {
1143        let maximum_bytes = limits.maximum_pack_bytes.min(self.maximum_bytes);
1144        let pages: i64 = self
1145            .connection
1146            .query_row("PRAGMA page_count", [], |row| row.get(0))?;
1147        if u64::try_from(pages)
1148            .ok()
1149            .and_then(|pages| pages.checked_mul(PAGE_SIZE.unsigned_abs()))
1150            .is_none_or(|bytes| bytes > maximum_bytes)
1151        {
1152            return Err(too_long(maximum_bytes));
1153        }
1154        // An image's digest is already known. A file is hashed only once
1155        // the integrity check has read its pages, as PACK.md explains.
1156        let hashed = matches!(self.source, Source::Image { .. });
1157        if hashed {
1158            self.check_digest(maximum_bytes)?;
1159        }
1160        let integrity: String = self
1161            .connection
1162            .query_row("PRAGMA integrity_check", [], |row| row.get(0))?;
1163        if integrity != "ok" {
1164            return Err(Error::Rejected(
1165                ErrorCode::Integrity,
1166                format!("integrity check failed: {integrity}"),
1167            ));
1168        }
1169        if !hashed {
1170            self.check_digest(maximum_bytes)?;
1171        }
1172        self.verify_contents(limits.maximum_decoded_bytes)
1173    }
1174
1175    /// Compares the pack's digest, reading at most `maximum_bytes`, with
1176    /// the one verification expects, if any.
1177    fn check_digest(&self, maximum_bytes: u64) -> Result<(), Error> {
1178        let Some(expected) = self.expected_digest() else {
1179            return Ok(());
1180        };
1181        // A name must be the lowercase digest; other cases do not match.
1182        let digest = crate::compile::hex(&self.digest_within(maximum_bytes)?);
1183        if digest != expected {
1184            return Err(Error::Rejected(
1185                ErrorCode::Digest,
1186                match self.source {
1187                    Source::File(_) => format!("file digest {digest} does not match its name"),
1188                    _ => format!("pack digest {digest} is not {expected}, the expected digest"),
1189                },
1190            ));
1191        }
1192        Ok(())
1193    }
1194
1195    /// The digest verification expects, in hex: a file's name when it is 64
1196    /// hex digits, or the digest an image was opened with.
1197    fn expected_digest(&self) -> Option<String> {
1198        match &self.source {
1199            Source::File(path) => path
1200                .file_name()
1201                .and_then(|name| name.to_str())
1202                .and_then(|name| name.strip_suffix(".twpack"))
1203                .filter(|stem| {
1204                    stem.len() == 64 && stem.bytes().all(|byte| byte.is_ascii_hexdigit())
1205                })
1206                .map(str::to_owned),
1207            Source::Image { expected, .. } => {
1208                expected.as_ref().map(|digest| crate::compile::hex(digest))
1209            }
1210            Source::Compiled => None,
1211        }
1212    }
1213
1214    /// Everything [`Pack::verify`] checks except SQLite integrity and the
1215    /// file name: what the compiler checks of every pack it builds.
1216    pub(crate) fn verify_contents(&self, maximum_decoded_bytes: u64) -> Result<Summary, Error> {
1217        let mut summary = self.summary()?;
1218        let (minimum, maximum): (Option<i64>, Option<i64>) =
1219            self.connection
1220                .query_row("SELECT min(id), max(id) FROM strings", [], |row| {
1221                    Ok((row.get(0)?, row.get(1)?))
1222                })?;
1223        let contiguous = match (minimum, maximum) {
1224            (None, None) => summary.strings == 0,
1225            (Some(1), Some(maximum)) => u64::try_from(maximum).ok() == Some(summary.strings),
1226            _ => false,
1227        };
1228        if !contiguous {
1229            return Err(Error::Rejected(
1230                ErrorCode::StringIds,
1231                "string IDs are not 1..=count".into(),
1232            ));
1233        }
1234        self.check_rows()?;
1235        self.check_zone_trees()?;
1236        self.check_string_encoding()?;
1237        self.check_documents()?;
1238        self.check_stairwells()?;
1239        let (render_layers, ground_layers, _) = self.profile_layers()?;
1240        let strings = u32::try_from(summary.strings)
1241            .map_err(|_| Error::Rejected(ErrorCode::StringIds, "too many strings".into()))?;
1242        let mut targets = Targets::new(&self.connection, strings, render_layers)?;
1243        let mut statement = self.connection.prepare(&format!(
1244            "SELECT p.id, p.chunk_size, c.layer, c.cx, c.cy, {} FROM chunks c JOIN planes p ON p.id = c.plane ORDER BY c.plane, c.layer, c.cx, c.cy",
1245            chunk_data_sql()
1246        ))?;
1247        let mut rows = statement.query([])?;
1248        let mut checked = 0;
1249        let mut decoded: u64 = 0;
1250        while let Some(row) = rows.next()? {
1251            let plane: String = row.get(0)?;
1252            let chunk_size: u16 = row.get(1)?;
1253            let key: (i32, i64, i64) = (row.get(2)?, row.get(3)?, row.get(4)?);
1254            let data: types::Value = row.get(5)?;
1255            // The frame records its decoded size, so the budget is checked
1256            // before the chunk is decompressed.
1257            decoded = decoded.saturating_add(recorded_size(&data));
1258            if decoded > maximum_decoded_bytes {
1259                return Err(Error::Rejected(
1260                    ErrorCode::DecodeBudget,
1261                    format!(
1262                        "the chunks decode to more than {maximum_decoded_bytes} bytes, the verification limit"
1263                    ),
1264                ));
1265            }
1266            // Objects are checked as they decode rather than kept, so memory
1267            // stays bounded by the record, not by the objects it holds.
1268            targets.enter(&plane, key);
1269            let mut failure = None;
1270            let mut visit = |object: &ObjectRecord| {
1271                summary.objects += 1;
1272                targets.check(object).map_err(|error| {
1273                    failure = Some(error);
1274                    DecodeError("an object failed verification".into())
1275                })
1276            };
1277            let decoded_chunk = chunk_data_with(&data, Some(&mut visit));
1278            if let Some(error) = failure {
1279                return Err(error);
1280            }
1281            let chunk = decoded_chunk?;
1282            if chunk.chunk_size != chunk_size || chunk.ground.len() != ground_layers {
1283                return Err(Error::Rejected(
1284                    ErrorCode::Chunk,
1285                    "a chunk disagrees with its plane or profile".into(),
1286                ));
1287            }
1288            targets.check_palette(&chunk)?;
1289            checked += 1;
1290        }
1291        if checked != summary.chunks {
1292            return Err(Error::Rejected(
1293                ErrorCode::Chunk,
1294                "a chunk names an unknown plane".into(),
1295            ));
1296        }
1297        // Each object with a uid matched the row of its uid, which names
1298        // only that object, so the rows the objects matched are distinct;
1299        // with as many rows as such objects, no row is left over and no two
1300        // objects share a uid.
1301        if targets.uids != summary.object_uids {
1302            return Err(Error::Rejected(
1303                ErrorCode::ObjectUid,
1304                "object_uids holds a uid that no object has".into(),
1305            ));
1306        }
1307        Ok(summary)
1308    }
1309
1310    /// Checks string, plane, zone, anchor and definition rows on their own
1311    /// and against each other, without loading them.
1312    fn check_rows(&self) -> Result<(), Error> {
1313        let checks = [
1314            (
1315                format!(
1316                    "SELECT count(*) FROM strings WHERE typeof(value) != 'text' OR octet_length(value) > {} OR instr(value, char(0)) != 0",
1317                    maximum_string_bytes()
1318                ),
1319                ErrorCode::String,
1320                "a string is not text of at most 1 MiB without NUL",
1321            ),
1322            (
1323                "SELECT count(*) FROM strings a JOIN strings b ON b.id = a.id + 1 WHERE b.value <= a.value".to_owned(),
1324                ErrorCode::StringIds,
1325                "string IDs are not in byte order of their values",
1326            ),
1327            (
1328                format!(
1329                    "SELECT count(*) FROM planes WHERE {} OR kind NOT IN ('shared', 'instanced', 'per_player') OR typeof(chunk_size) != 'integer' OR chunk_size != {CHUNK_SIZE}",
1330                    not_identifier("id")
1331                ),
1332                ErrorCode::Plane,
1333                "a plane has an invalid ID or kind, or a chunk size other than 32",
1334            ),
1335            (
1336                format!(
1337                    "SELECT count(*) FROM zones z WHERE {} OR NOT EXISTS (SELECT 1 FROM planes p WHERE p.id = z.plane) OR (z.parent IS NOT NULL AND NOT EXISTS (SELECT 1 FROM zones q WHERE q.plane = z.plane AND q.id = z.parent))",
1338                    not_identifier("z.id")
1339                ),
1340                ErrorCode::Zone,
1341                "a zone has an invalid ID or names an unknown plane or parent",
1342            ),
1343            (
1344                format!(
1345                    "SELECT count(*) FROM anchors a WHERE {} OR NOT EXISTS (SELECT 1 FROM planes p WHERE p.id = a.plane) OR typeof(a.layer) != 'integer' OR a.layer NOT BETWEEN -2147483648 AND 2147483647 OR typeof(a.x) != 'integer' OR a.x NOT BETWEEN -{MAX_SAFE_INTEGER} AND {MAX_SAFE_INTEGER} OR typeof(a.y) != 'integer' OR a.y NOT BETWEEN -{MAX_SAFE_INTEGER} AND {MAX_SAFE_INTEGER}",
1346                    not_identifier("a.id")
1347                ),
1348                ErrorCode::Anchor,
1349                "an anchor has an invalid ID, layer or position, or names an unknown plane",
1350            ),
1351            (
1352                format!(
1353                    "SELECT count(*) FROM stairwells s WHERE {} OR NOT EXISTS (SELECT 1 FROM planes p WHERE p.id = s.plane)",
1354                    not_identifier("s.id")
1355                ),
1356                ErrorCode::Stairwell,
1357                "a stairwell has an invalid ID or names an unknown plane",
1358            ),
1359            (
1360                // A step's stairwell is a stairwell of its plane, so the plane
1361                // exists too.
1362                format!(
1363                    "SELECT count(*) FROM stairwell_steps t WHERE NOT EXISTS (SELECT 1 FROM stairwells s WHERE s.plane = t.plane AND s.id = t.stairwell) \
1364                     OR typeof(t.layer) != 'integer' OR t.layer NOT BETWEEN -2147483648 AND 2147483647 \
1365                     OR typeof(t.x) != 'integer' OR t.x NOT BETWEEN -{MAX_SAFE_INTEGER} AND {MAX_SAFE_INTEGER} \
1366                     OR typeof(t.y) != 'integer' OR t.y NOT BETWEEN -{MAX_SAFE_INTEGER} AND {MAX_SAFE_INTEGER} \
1367                     OR {} IS NULL \
1368                     OR {} OR NOT EXISTS (SELECT 1 FROM definitions d WHERE d.type = 'proto' AND d.id = t.proto)",
1369                    step_index("t.step"),
1370                    not_identifier("t.proto")
1371                ),
1372                ErrorCode::StairwellStep,
1373                "a stairwell step names an unknown stairwell or proto, or has an invalid layer, position or index",
1374            ),
1375            (
1376                // Chunk `c` covers tiles `c * size ..= c * size + size - 1`.
1377                format!(
1378                    "SELECT count(*) FROM chunks c LEFT JOIN planes p ON p.id = c.plane WHERE p.id IS NULL \
1379                     OR typeof(c.layer) != 'integer' OR c.layer NOT BETWEEN -2147483648 AND 2147483647 \
1380                     OR typeof(c.cx) != 'integer' OR typeof(c.cy) != 'integer' \
1381                     OR c.cx NOT BETWEEN -({MAX_SAFE_INTEGER} / p.chunk_size) AND ({MAX_SAFE_INTEGER} - p.chunk_size + 1) / p.chunk_size \
1382                     OR c.cy NOT BETWEEN -({MAX_SAFE_INTEGER} / p.chunk_size) AND ({MAX_SAFE_INTEGER} - p.chunk_size + 1) / p.chunk_size"
1383                ),
1384                ErrorCode::Chunk,
1385                "a chunk names an unknown plane or has an invalid layer or position",
1386            ),
1387            (
1388                format!(
1389                    "SELECT count(*) FROM definitions WHERE {} OR {}",
1390                    not_identifier("type"),
1391                    not_identifier("id")
1392                ),
1393                ErrorCode::Definition,
1394                "a definition type or ID is not an identifier",
1395            ),
1396            (
1397                // The row's chunk exists, so its plane, layer and position
1398                // are valid too.
1399                format!(
1400                    "SELECT count(*) FROM object_uids u WHERE {} \
1401                     OR typeof(u.layer) != 'integer' OR typeof(u.cx) != 'integer' OR typeof(u.cy) != 'integer' \
1402                     OR typeof(u.object) != 'integer' OR u.object NOT BETWEEN 0 AND {MAXIMUM_OBJECT_INDEX} \
1403                     OR NOT EXISTS (SELECT 1 FROM chunks c WHERE c.plane = u.plane AND c.layer = u.layer AND c.cx = u.cx AND c.cy = u.cy)",
1404                    not_identifier("u.uid")
1405                ),
1406                ErrorCode::ObjectUid,
1407                "an object_uids row has an invalid uid or index, or names a missing chunk",
1408            ),
1409        ];
1410        for (query, code, problem) in checks {
1411            let count: i64 = self.connection.query_row(&query, [], |row| row.get(0))?;
1412            if count != 0 {
1413                return Err(Error::Rejected(code, problem.into()));
1414            }
1415        }
1416        Ok(())
1417    }
1418
1419    /// Checks that each plane has at most as many zones as one source
1420    /// document holds, and that their parent chains end at a root. Planes
1421    /// are checked one at a time, so what SQLite keeps while following the
1422    /// chains is bounded by that limit rather than by the pack.
1423    /// [`Pack::check_rows`] has already checked that every zone's plane
1424    /// and parent exist.
1425    fn check_zone_trees(&self) -> Result<(), Error> {
1426        let limit = i64::try_from(Limits::default().max_array_items).unwrap_or(i64::MAX);
1427        let mut planes = self
1428            .connection
1429            .prepare("SELECT id FROM planes ORDER BY id")?;
1430        let mut count = self
1431            .connection
1432            .prepare("SELECT count(*) FROM zones WHERE plane = ?1")?;
1433        // The plane's zones reached from a root by following children: with
1434        // one parent each, a zone is reached once or, on or below a cycle,
1435        // never.
1436        let mut unrooted = self.connection.prepare(
1437            "WITH RECURSIVE children(parent, id) AS MATERIALIZED \
1438             (SELECT parent, id FROM zones WHERE plane = ?1 AND parent IS NOT NULL), \
1439             rooted(id) AS (SELECT id FROM zones WHERE plane = ?1 AND parent IS NULL \
1440             UNION ALL SELECT c.id FROM rooted r JOIN children c ON c.parent = r.id) \
1441             SELECT (SELECT count(*) FROM zones WHERE plane = ?1) - (SELECT count(*) FROM rooted)",
1442        )?;
1443        let mut rows = planes.query([])?;
1444        while let Some(row) = rows.next()? {
1445            let plane: String = row.get(0)?;
1446            let zones: i64 = count.query_row([&plane], |row| row.get(0))?;
1447            if zones > limit {
1448                return Err(Error::Rejected(
1449                    ErrorCode::Zone,
1450                    format!("plane {plane} has more zones than a source document holds"),
1451                ));
1452            }
1453            let missing: i64 = unrooted.query_row([&plane], |row| row.get(0))?;
1454            if missing != 0 {
1455                return Err(Error::Rejected(
1456                    ErrorCode::Zone,
1457                    "zone parents form a cycle".into(),
1458                ));
1459            }
1460        }
1461        Ok(())
1462    }
1463
1464    /// Checks that every string is UTF-8, one bounded row at a time; SQLite
1465    /// accepts any bytes as text.
1466    fn check_string_encoding(&self) -> Result<(), Error> {
1467        let mut statement = self
1468            .connection
1469            .prepare("SELECT value FROM strings ORDER BY id")?;
1470        let mut rows = statement.query([])?;
1471        while let Some(row) = rows.next()? {
1472            match row.get_ref(0)? {
1473                types::ValueRef::Text(bytes) if std::str::from_utf8(bytes).is_ok() => {}
1474                _ => {
1475                    return Err(Error::Rejected(
1476                        ErrorCode::String,
1477                        "a string is not UTF-8 text".into(),
1478                    ));
1479                }
1480            }
1481        }
1482        Ok(())
1483    }
1484
1485    /// Checks that every `document` column holds canonical JSON of an
1486    /// object whose identity keys agree with its row.
1487    fn check_documents(&self) -> Result<(), Error> {
1488        let limit = maximum_document_bytes();
1489        let document = bounded_text("document");
1490        let mut planes = self.connection.prepare(&format!(
1491            "SELECT id, kind, chunk_size, {document} FROM planes ORDER BY id"
1492        ))?;
1493        let mut has_zone = self
1494            .connection
1495            .prepare("SELECT EXISTS (SELECT 1 FROM zones WHERE plane = ?1 AND id = ?2)")?;
1496        let mut rows = planes.query([limit])?;
1497        while let Some(row) = rows.next()? {
1498            let (id, kind, chunk_size): (String, String, i64) =
1499                (row.get(0)?, row.get(1)?, row.get(2)?);
1500            let stored = stored_document(
1501                utf8_column(row, 3, ErrorCode::Plane)?,
1502                "a plane document",
1503                ErrorCode::Plane,
1504            )?;
1505            let default_zone_ok = match stored.root().get("default_zone") {
1506                None => true,
1507                Some(zone) => match zone.as_str() {
1508                    Some(zone) => has_zone.query_row(params![id, zone], |row| row.get(0))?,
1509                    None => false,
1510                },
1511            };
1512            if text(&stored, "format") != Some("tilewright.plane/1")
1513                || text(&stored, "id") != Some(&id)
1514                || text(&stored, "kind") != Some(&kind)
1515                || stored.root().get("chunk_size").and_then(Value::as_i64) != Some(chunk_size)
1516                || !default_zone_ok
1517            {
1518                return Err(Error::Rejected(
1519                    ErrorCode::Plane,
1520                    format!("the document of plane {id} disagrees with its row"),
1521                ));
1522            }
1523        }
1524        let mut zones = self.connection.prepare(&format!(
1525            "SELECT plane, id, parent, {document} FROM zones ORDER BY plane, id"
1526        ))?;
1527        let mut rows = zones.query([limit])?;
1528        while let Some(row) = rows.next()? {
1529            let (plane, id, parent): (String, String, Option<String>) =
1530                (row.get(0)?, row.get(1)?, row.get(2)?);
1531            let stored = stored_document(
1532                utf8_column(row, 3, ErrorCode::Zone)?,
1533                "a zone document",
1534                ErrorCode::Zone,
1535            )?;
1536            if text(&stored, "id") != Some(&id)
1537                || stored.root().get("parent").map(Value::as_str) != parent.as_deref().map(Some)
1538            {
1539                return Err(Error::Rejected(
1540                    ErrorCode::Zone,
1541                    format!("the document of zone {plane}/{id} disagrees with its row"),
1542                ));
1543            }
1544        }
1545        let mut definitions = self.connection.prepare(&format!(
1546            "SELECT type, id, {document} FROM definitions ORDER BY type, id"
1547        ))?;
1548        let mut rows = definitions.query([limit])?;
1549        while let Some(row) = rows.next()? {
1550            let (kind, id): (String, String) = (row.get(0)?, row.get(1)?);
1551            let stored = stored_document(
1552                utf8_column(row, 2, ErrorCode::Definition)?,
1553                "a definition document",
1554                ErrorCode::Definition,
1555            )?;
1556            if text(&stored, "format") != Some("tilewright.def/1")
1557                || text(&stored, "type") != Some(&kind)
1558                || text(&stored, "id") != Some(&id)
1559            {
1560                return Err(Error::Rejected(
1561                    ErrorCode::Definition,
1562                    format!("the document of definition {kind}/{id} disagrees with its row"),
1563                ));
1564            }
1565        }
1566        Ok(())
1567    }
1568
1569    /// Checks every stored stairwell document against its row and the
1570    /// `stairwell_steps` table: each step is the row at its tile, naming its
1571    /// stairwell, its index and its prototype, and the table holds no other
1572    /// rows. One document is held at a time, and each step is one lookup.
1573    /// [`Pack::check_rows`] has already checked the rows' columns.
1574    fn check_stairwells(&self) -> Result<(), Error> {
1575        let mut stairwells = self.connection.prepare(&format!(
1576            "SELECT plane, id, {} FROM stairwells ORDER BY plane, id",
1577            bounded_text("document")
1578        ))?;
1579        let mut step_at = self.connection.prepare(
1580            "SELECT stairwell, step, proto FROM stairwell_steps WHERE plane = ?1 AND layer = ?2 AND x = ?3 AND y = ?4",
1581        )?;
1582        let mut listed: u64 = 0;
1583        // Stairwells and steps of the plane being checked: one source
1584        // document holds at most the array limit of stairwells and the value
1585        // limit of steps.
1586        let limits = Limits::default();
1587        let (mut current, mut count, mut steps) = (String::new(), 0_usize, 0_usize);
1588        let mut rows = stairwells.query([maximum_document_bytes()])?;
1589        while let Some(row) = rows.next()? {
1590            let (plane, id): (String, String) = (row.get(0)?, row.get(1)?);
1591            if plane != current {
1592                current.clone_from(&plane);
1593                (count, steps) = (0, 0);
1594            }
1595            count += 1;
1596            if count > limits.max_array_items {
1597                return Err(Error::Rejected(
1598                    ErrorCode::Stairwell,
1599                    format!("plane {plane} has more stairwells than a source document holds"),
1600                ));
1601            }
1602            let stored = stored_stairwell(&stored_document(
1603                utf8_column(row, 2, ErrorCode::Stairwell)?,
1604                "a stairwell document",
1605                ErrorCode::Stairwell,
1606            )?)?;
1607            steps = steps.saturating_add(stored.steps.len());
1608            if steps > limits.max_values {
1609                return Err(Error::Rejected(
1610                    ErrorCode::Stairwell,
1611                    format!("plane {plane} has more stairwell steps than a source document holds"),
1612                ));
1613            }
1614            let disagrees = || {
1615                Error::Rejected(
1616                    ErrorCode::Stairwell,
1617                    format!("the document of stairwell {plane}/{id} disagrees with its rows"),
1618                )
1619            };
1620            if stored.id != id {
1621                return Err(disagrees());
1622            }
1623            for (index, step) in stored.steps.iter().enumerate() {
1624                let found: Option<(String, i64, String)> = step_at
1625                    .query_row(params![plane, step.layer, step.at[0], step.at[1]], |row| {
1626                        Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1627                    })
1628                    .optional()?;
1629                let expected = i64::try_from(index).map_err(|_| disagrees())?;
1630                if found.as_ref().is_none_or(|(stairwell, step_index, proto)| {
1631                    *stairwell != id || *step_index != expected || *proto != step.proto
1632                }) {
1633                    return Err(disagrees());
1634                }
1635            }
1636            listed = listed.saturating_add(u64::try_from(stored.steps.len()).unwrap_or(u64::MAX));
1637        }
1638        let rows: i64 =
1639            self.connection
1640                .query_row("SELECT count(*) FROM stairwell_steps", [], |row| row.get(0))?;
1641        if u64::try_from(rows).ok() != Some(listed) {
1642            return Err(Error::Rejected(
1643                ErrorCode::StairwellStep,
1644                "stairwell_steps holds a step that no stairwell lists".into(),
1645            ));
1646        }
1647        Ok(())
1648    }
1649
1650    /// Render and ground layer counts from the stored profile, which must be
1651    /// canonical and agree with `profile_id` and `profile_version`.
1652    fn profile_layers(&self) -> Result<(usize, usize, Option<u32>), Error> {
1653        let profile = stored_document(
1654            self.meta("profile")?,
1655            "the stored profile",
1656            ErrorCode::Profile,
1657        )?;
1658        // The identity must be one a source profile can have: an identifier
1659        // and a version of at most 64 characters.
1660        let identity_ok = text(&profile, "id").is_some_and(is_identifier)
1661            && text(&profile, "version").is_some_and(|version| version.chars().count() <= 64);
1662        if text(&profile, "format") != Some("tilewright.profile/1")
1663            || !identity_ok
1664            || text(&profile, "id") != self.meta("profile_id")?.as_deref()
1665            || text(&profile, "version") != self.meta("profile_version")?.as_deref()
1666        {
1667            return Err(Error::Rejected(
1668                ErrorCode::Profile,
1669                "the stored profile disagrees with profile_id or profile_version".into(),
1670            ));
1671        }
1672        if !projection_ok(&profile) {
1673            return Err(Error::Rejected(
1674                ErrorCode::Profile,
1675                "the stored profile's projection is not one a source profile can have".into(),
1676            ));
1677        }
1678        let layer_height = stored_layer_height(&profile)?;
1679        // Each list holds unique identifiers within its count, and every
1680        // ground layer is a render layer other than `height` or `zone`.
1681        let layers = |key: &str, range: std::ops::RangeInclusive<usize>| {
1682            let names: Option<Vec<&str>> = profile
1683                .root()
1684                .get(key)
1685                .and_then(Value::as_array)
1686                .filter(|items| range.contains(&items.len()))
1687                .and_then(|items| items.iter().map(Value::as_str).collect());
1688            names
1689                .filter(|names| {
1690                    let unique: BTreeSet<&str> = names.iter().copied().collect();
1691                    unique.len() == names.len() && names.iter().all(|name| is_identifier(name))
1692                })
1693                .ok_or_else(|| {
1694                    Error::Rejected(
1695                        ErrorCode::Profile,
1696                        format!(
1697                            "stored profile does not list {}..={} unique {key}",
1698                            range.start(),
1699                            range.end()
1700                        ),
1701                    )
1702                })
1703        };
1704        let render = layers("render_layers", 1..=usize::from(u16::MAX - 1))?;
1705        let ground = layers("ground_layers", 0..=MAXIMUM_GROUND_LAYERS)?;
1706        let known: BTreeSet<&str> = render.iter().copied().collect();
1707        if ground
1708            .iter()
1709            .any(|name| !known.contains(name) || ["height", "zone"].contains(name))
1710        {
1711            return Err(Error::Rejected(
1712                ErrorCode::Profile,
1713                "a stored ground layer is not a render layer other than height or zone".into(),
1714            ));
1715        }
1716        Ok((render.len(), ground.len(), layer_height))
1717    }
1718}
1719
1720/// A pack image being read into SQLite's copy. Reading it never fails:
1721/// once the image fails or ends, the rest of the copy is zeros, and whether
1722/// it ended early or the error it failed with is kept for the caller.
1723/// `rusqlite` would report either as an SQLite error, not as the I/O error
1724/// it is.
1725struct Filled<R> {
1726    inner: R,
1727    missing: bool,
1728    failure: Option<std::io::Error>,
1729    /// The first [`HEADER_BYTES`] bytes `inner` yielded.
1730    head: Vec<u8>,
1731    /// Every byte `inner` yielded: the copy, when `inner` yields the whole
1732    /// image.
1733    hasher: Sha256,
1734}
1735
1736impl<R: std::io::Read> std::io::Read for Filled<R> {
1737    fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1738        if buffer.is_empty() {
1739            return Ok(0);
1740        }
1741        while !self.missing && self.failure.is_none() {
1742            match self.inner.read(buffer) {
1743                Ok(0) => self.missing = true,
1744                Ok(read) => {
1745                    let wanted = HEADER_BYTES.saturating_sub(self.head.len()).min(read);
1746                    self.head.extend_from_slice(&buffer[..wanted]);
1747                    self.hasher.update(&buffer[..read]);
1748                    return Ok(read);
1749                }
1750                Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
1751                Err(error) => self.failure = Some(error),
1752            }
1753        }
1754        buffer.fill(0);
1755        Ok(buffer.len())
1756    }
1757}
1758
1759/// How much of a pack's start [`check_header`] reads.
1760const HEADER_BYTES: usize = 20;
1761
1762/// Checks the start of a pack, `head`, before SQLite reads it. A database
1763/// file begins with SQLite's header string, and its header bytes 18 and 19,
1764/// the file format write and read versions, are 1 for a rollback journal,
1765/// as in every pack, or 2 for WAL. SQLite opens a WAL database from an
1766/// immutable file but not from an in-memory copy, so every reader refuses
1767/// one, as from any other version, before SQLite reads it. Bytes that are
1768/// not a database are left for SQLite to refuse.
1769fn check_header(head: &[u8]) -> Result<(), Error> {
1770    if head.len() == HEADER_BYTES
1771        && head.starts_with(b"SQLite format 3\0")
1772        && (head[18] != 1 || head[19] != 1)
1773    {
1774        return Err(Error::Rejected(
1775            ErrorCode::Identity,
1776            "the SQLite header's file format versions are not 1, a rollback journal's".into(),
1777        ));
1778    }
1779    Ok(())
1780}
1781
1782/// Whether `read` yields at least one more byte.
1783fn read_some(read: &mut impl std::io::Read) -> Result<bool, Error> {
1784    loop {
1785        match read.read(&mut [0]) {
1786            Ok(read) => return Ok(read != 0),
1787            Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
1788            Err(error) => return Err(Error::Io(error)),
1789        }
1790    }
1791}
1792
1793/// Puts a connection to a pack in SQLite's defensive mode, with untrusted
1794/// schemas disabled and writes refused.
1795fn harden(connection: &Connection) -> Result<(), Error> {
1796    connection.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?;
1797    connection.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?;
1798    connection.execute_batch("PRAGMA query_only = ON")?;
1799    Ok(())
1800}
1801
1802/// Checks that a pack of `length` bytes is exactly its pages, and that they
1803/// are within the pack size limit. Nothing, not even a sparse hole, follows
1804/// the last page, so hashing a pack never reads past its pages.
1805fn check_pages(connection: &Connection, length: u64, limits: &VerifyLimits) -> Result<(), Error> {
1806    let pages: i64 = connection.query_row("PRAGMA page_count", [], |row| row.get(0))?;
1807    if u64::try_from(pages)
1808        .ok()
1809        .and_then(|pages| pages.checked_mul(PAGE_SIZE.unsigned_abs()))
1810        != Some(length)
1811    {
1812        return Err(Error::Rejected(
1813            ErrorCode::FileLength,
1814            format!("the file is {length} bytes, not its {pages} pages of {PAGE_SIZE} bytes"),
1815        ));
1816    }
1817    if length > limits.maximum_pack_bytes {
1818        return Err(too_long(limits.maximum_pack_bytes));
1819    }
1820    Ok(())
1821}
1822
1823/// Checks the schema, application ID, user version and `meta` of a pack
1824/// before any other query runs.
1825fn check_identity(connection: &Connection) -> Result<(), Error> {
1826    // At most one row more than expected, each value bounded, is loaded.
1827    let mut schema = connection.prepare(&format!(
1828        "SELECT {}, {}, {} FROM sqlite_schema WHERE sql IS NOT NULL ORDER BY name LIMIT {}",
1829        bounded_text("type"),
1830        bounded_text("name"),
1831        bounded_text("sql"),
1832        SCHEMA_TABLES.len() + 1
1833    ))?;
1834    let mut found: Vec<(Option<String>, Option<String>, Option<String>)> = Vec::new();
1835    let mut rows = schema.query([bound(MAXIMUM_SCHEMA_BYTES)])?;
1836    while let Some(row) = rows.next()? {
1837        found.push((
1838            utf8_column(row, 0, ErrorCode::Schema)?,
1839            utf8_column(row, 1, ErrorCode::Schema)?,
1840            utf8_column(row, 2, ErrorCode::Schema)?,
1841        ));
1842    }
1843    drop(rows);
1844    drop(schema);
1845    let mut expected: Vec<(Option<String>, Option<String>, Option<String>)> = SCHEMA_TABLES
1846        .iter()
1847        .map(|(name, sql)| {
1848            (
1849                Some("table".to_owned()),
1850                Some((*name).to_owned()),
1851                Some((*sql).to_owned()),
1852            )
1853        })
1854        .collect();
1855    expected.sort_by(|left, right| left.1.cmp(&right.1));
1856    if found != expected {
1857        return Err(Error::Rejected(
1858            ErrorCode::Schema,
1859            "the pack schema does not match this format".into(),
1860        ));
1861    }
1862    let application: i32 = connection.query_row("PRAGMA application_id", [], |row| row.get(0))?;
1863    let version: i32 = connection.query_row("PRAGMA user_version", [], |row| row.get(0))?;
1864    let page_size: i64 = connection.query_row("PRAGMA page_size", [], |row| row.get(0))?;
1865    if application != APPLICATION_ID || version != USER_VERSION || page_size != PAGE_SIZE {
1866        return Err(Error::Rejected(
1867            ErrorCode::Identity,
1868            format!(
1869                "not a Tilewright pack of user_version {USER_VERSION} with {PAGE_SIZE}-byte pages"
1870            ),
1871        ));
1872    }
1873    // `VACUUM` leaves a compiled pack no free pages. Free pages hold no
1874    // data, so a sparse file could make them holes that cost nothing to
1875    // store and much to hash; with none, the integrity check reads every
1876    // page as table data before the file is hashed.
1877    let free: i64 = connection.query_row("PRAGMA freelist_count", [], |row| row.get(0))?;
1878    if free != 0 {
1879        return Err(Error::Rejected(
1880            ErrorCode::FreePages,
1881            format!("the pack has {free} free pages; a pack has none"),
1882        ));
1883    }
1884    let meta = read_meta(connection)?;
1885    let value = |key: &str| {
1886        meta.iter()
1887            .find(|(name, _)| name == key)
1888            .map(|(_, value)| value.as_str())
1889    };
1890    let digest_ok = value("source_digest").is_some_and(|digest| {
1891        digest.len() == 64
1892            && digest
1893                .bytes()
1894                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1895    });
1896    if value("format") != Some(FORMAT)
1897        || value("chunk_encoding") != Some(CHUNK_ENCODING)
1898        || !digest_ok
1899    {
1900        return Err(Error::Rejected(
1901            ErrorCode::Meta,
1902            format!("the pack's metadata is not that of {FORMAT}"),
1903        ));
1904    }
1905    Ok(())
1906}
1907
1908/// Reads `meta` after checking, without loading any value, that it holds
1909/// exactly the format's keys with text values of bounded size.
1910fn read_meta(connection: &Connection) -> Result<Vec<(String, String)>, Error> {
1911    let keys = META_KEYS
1912        .iter()
1913        .map(|key| format!("'{key}'"))
1914        .collect::<Vec<_>>()
1915        .join(", ");
1916    let (total, known, oversized): (i64, i64, i64) = connection.query_row(
1917        &format!(
1918            "SELECT count(*), count(*) FILTER (WHERE key IN ({keys})), \
1919             count(*) FILTER (WHERE typeof(value) != 'text' OR octet_length(value) > CASE key WHEN 'profile' THEN ?1 ELSE ?2 END) \
1920             FROM meta"
1921        ),
1922        params![maximum_document_bytes(), bound(MAXIMUM_META_BYTES)],
1923        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1924    )?;
1925    let expected = i64::try_from(META_KEYS.len()).unwrap_or(i64::MAX);
1926    if total != expected || known != expected || oversized != 0 {
1927        return Err(Error::Rejected(
1928            ErrorCode::Meta,
1929            format!("the pack's metadata is not that of {FORMAT}"),
1930        ));
1931    }
1932    let mut statement = connection.prepare("SELECT key, value FROM meta ORDER BY key")?;
1933    let mut rows = statement.query([])?;
1934    let mut meta = Vec::with_capacity(META_KEYS.len());
1935    while let Some(row) = rows.next()? {
1936        // Every value was checked to be text above.
1937        let value = utf8_column(row, 1, ErrorCode::Meta)?.unwrap_or_default();
1938        meta.push((row.get(0)?, value));
1939    }
1940    Ok(meta)
1941}
1942
1943/// What chunk records may reference. Strings are checked by ID, since
1944/// verification has proven IDs are `1..=strings`. Every other lookup is
1945/// cached per distinct ID in a cache of bounded size, so checking an object
1946/// costs map lookups and at worst a query per object: memory stays bounded
1947/// and work stays proportional to the decoded bytes its budget bounds.
1948struct Targets<'c> {
1949    strings: u32,
1950    render_layers: usize,
1951    /// Whether string `?1` names a plane.
1952    is_plane: Statement<'c>,
1953    /// Whether string `?1` names a `proto` definition.
1954    is_proto: Statement<'c>,
1955    /// Whether string `?1` names a zone of plane `?2`.
1956    is_zone: Statement<'c>,
1957    /// Whether plane `?1` (a string ID, or 0 for plane `?2`) has the anchor
1958    /// named by string `?3`.
1959    has_anchor: Statement<'c>,
1960    /// The value of string `?1`.
1961    value: Statement<'c>,
1962    planes: BTreeMap<u32, bool>,
1963    protos: BTreeMap<u32, bool>,
1964    field_names: BTreeMap<u32, bool>,
1965    /// Anchors by `(plane, anchor)` for portals that name their plane.
1966    anchors: BTreeMap<(u32, u32), bool>,
1967    /// The plane of the chunks being checked, and the lookups relative to
1968    /// it: its zones and its own anchors.
1969    current: String,
1970    zones: BTreeMap<u32, bool>,
1971    own_anchors: BTreeMap<u32, bool>,
1972    /// The `object_uids` row of uid `?1`.
1973    uid_row: Statement<'c>,
1974    /// The layer and position of the chunk being checked, the index of its
1975    /// next object in the record's depth-first order, and the objects with
1976    /// a uid checked so far.
1977    chunk: (i32, i64, i64),
1978    index: i64,
1979    uids: u64,
1980}
1981
1982impl<'c> Targets<'c> {
1983    /// The lookups of a verification whose strings are `1..=strings` and
1984    /// whose profile has `render_layers` render layers.
1985    fn new(connection: &'c Connection, strings: u32, render_layers: usize) -> Result<Self, Error> {
1986        Ok(Self {
1987            strings,
1988            render_layers,
1989            is_plane: connection.prepare(
1990                "SELECT EXISTS (SELECT 1 FROM strings s JOIN planes p ON p.id = s.value WHERE s.id = ?1)",
1991            )?,
1992            is_proto: connection.prepare(
1993                "SELECT EXISTS (SELECT 1 FROM strings s JOIN definitions d ON d.type = 'proto' AND d.id = s.value WHERE s.id = ?1)",
1994            )?,
1995            is_zone: connection.prepare(
1996                "SELECT EXISTS (SELECT 1 FROM strings s JOIN zones z ON z.plane = ?2 AND z.id = s.value WHERE s.id = ?1)",
1997            )?,
1998            has_anchor: connection.prepare(
1999                "SELECT EXISTS (SELECT 1 FROM strings n JOIN anchors a ON a.id = n.value \
2000                 WHERE n.id = ?3 AND a.plane = CASE WHEN ?1 = 0 THEN ?2 ELSE (SELECT value FROM strings WHERE id = ?1) END)",
2001            )?,
2002            value: connection.prepare("SELECT value FROM strings WHERE id = ?1")?,
2003            uid_row: connection.prepare(
2004                "SELECT plane, layer, cx, cy, object FROM object_uids WHERE uid = ?1",
2005            )?,
2006            chunk: (0, 0, 0),
2007            index: 0,
2008            uids: 0,
2009            planes: BTreeMap::new(),
2010            protos: BTreeMap::new(),
2011            field_names: BTreeMap::new(),
2012            anchors: BTreeMap::new(),
2013            current: String::new(),
2014            zones: BTreeMap::new(),
2015            own_anchors: BTreeMap::new(),
2016        })
2017    }
2018}
2019
2020/// Entries kept per lookup cache. A full cache is cleared, so memory stays
2021/// bounded however many distinct targets a pack names, and a lookup is at
2022/// worst one query per object.
2023const CACHE_ENTRIES: usize = 1 << 16;
2024
2025/// A cached boolean lookup.
2026fn cached<K: Ord + Copy>(
2027    cache: &mut BTreeMap<K, bool>,
2028    key: K,
2029    lookup: impl FnOnce() -> Result<bool, Error>,
2030) -> Result<bool, Error> {
2031    if let Some(known) = cache.get(&key) {
2032        return Ok(*known);
2033    }
2034    let found = lookup()?;
2035    if cache.len() >= CACHE_ENTRIES {
2036        cache.clear();
2037    }
2038    cache.insert(key, found);
2039    Ok(found)
2040}
2041
2042impl Targets<'_> {
2043    fn string(&self, id: u32) -> bool {
2044        (1..=self.strings).contains(&id)
2045    }
2046
2047    /// Starts checking the chunk of `plane` at `chunk`, dropping lookups
2048    /// relative to the previous plane.
2049    fn enter(&mut self, plane: &str, chunk: (i32, i64, i64)) {
2050        if self.current != plane {
2051            plane.clone_into(&mut self.current);
2052            self.zones.clear();
2053            self.own_anchors.clear();
2054        }
2055        self.chunk = chunk;
2056        self.index = 0;
2057    }
2058
2059    /// Checks that an object's uid has the `object_uids` row that names the
2060    /// object: the current chunk and the object's index in it.
2061    fn uid(&mut self, object: &ObjectRecord) -> Result<(), Error> {
2062        let index = self.index;
2063        self.index += 1;
2064        let Some(uid) = &object.uid else {
2065            return Ok(());
2066        };
2067        self.uids += 1;
2068        let row: Option<(
2069            types::Value,
2070            types::Value,
2071            types::Value,
2072            types::Value,
2073            types::Value,
2074        )> = self
2075            .uid_row
2076            .query_row([uid], |row| {
2077                Ok((
2078                    row.get(0)?,
2079                    row.get(1)?,
2080                    row.get(2)?,
2081                    row.get(3)?,
2082                    row.get(4)?,
2083                ))
2084            })
2085            .optional()?;
2086        let (layer, cx, cy) = self.chunk;
2087        let expected = (
2088            types::Value::Text(self.current.clone()),
2089            types::Value::Integer(i64::from(layer)),
2090            types::Value::Integer(cx),
2091            types::Value::Integer(cy),
2092            types::Value::Integer(index),
2093        );
2094        if row.as_ref() == Some(&expected) {
2095            Ok(())
2096        } else {
2097            Err(Error::Rejected(
2098                ErrorCode::ObjectUid,
2099                format!("the object_uids row of {uid} does not name its object"),
2100            ))
2101        }
2102    }
2103
2104    fn proto(&mut self, id: u32) -> Result<bool, Error> {
2105        if !self.string(id) {
2106            return Ok(false);
2107        }
2108        let statement = &mut self.is_proto;
2109        cached(&mut self.protos, id, || {
2110            Ok(statement.query_row([id], |row| row.get(0))?)
2111        })
2112    }
2113
2114    fn field_name(&mut self, id: u32) -> Result<bool, Error> {
2115        if !self.string(id) {
2116            return Ok(false);
2117        }
2118        let statement = &mut self.value;
2119        cached(&mut self.field_names, id, || {
2120            let name: String = statement.query_row([id], |row| row.get(0))?;
2121            Ok(is_field_name(&name))
2122        })
2123    }
2124
2125    /// Checks that every palette entry is used, that ground cells name
2126    /// `proto` definitions, and that zone cells name zones of the current
2127    /// plane.
2128    fn check_palette(&mut self, chunk: &ChunkData) -> Result<(), Error> {
2129        fn mark(cells: &[u16], used: &mut [bool]) {
2130            for cell in cells {
2131                if let Some(flag) = usize::from(*cell)
2132                    .checked_sub(1)
2133                    .and_then(|index| used.get_mut(index))
2134                {
2135                    *flag = true;
2136                }
2137            }
2138        }
2139        let mut ground = vec![false; chunk.palette.len()];
2140        let mut zone = vec![false; chunk.palette.len()];
2141        for grid in &chunk.ground {
2142            mark(grid, &mut ground);
2143        }
2144        mark(&chunk.zones, &mut zone);
2145        for ((id, ground), zone) in chunk.palette.iter().zip(ground).zip(zone) {
2146            let id = *id;
2147            let mut ok = (ground || zone) && self.string(id) && (!ground || self.proto(id)?);
2148            if ok && zone {
2149                let (statement, plane) = (&mut self.is_zone, &self.current);
2150                ok = cached(&mut self.zones, id, || {
2151                    Ok(statement.query_row(params![id, plane], |row| row.get(0))?)
2152                })?;
2153            }
2154            if !ok {
2155                return Err(Error::Rejected(
2156                    ErrorCode::Reference,
2157                    "a palette entry is unused or not a proto definition or zone of its plane"
2158                        .into(),
2159                ));
2160            }
2161        }
2162        Ok(())
2163    }
2164
2165    fn portal(&mut self, portal: Option<&PortalRecord>) -> Result<bool, Error> {
2166        Ok(match portal {
2167            Some(PortalRecord::Location { x, y, .. })
2168                if [x, y]
2169                    .iter()
2170                    .any(|value| !(-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(*value)) =>
2171            {
2172                false
2173            }
2174            Some(PortalRecord::Location { plane: 0, .. }) | None => true,
2175            Some(PortalRecord::Location { plane, .. }) => {
2176                let plane = *plane;
2177                if !self.string(plane) {
2178                    return Ok(false);
2179                }
2180                let statement = &mut self.is_plane;
2181                cached(&mut self.planes, plane, || {
2182                    Ok(statement.query_row([plane], |row| row.get(0))?)
2183                })?
2184            }
2185            Some(PortalRecord::Anchor { plane, anchor }) => {
2186                let (plane, anchor) = (*plane, *anchor);
2187                if !(plane == 0 || self.string(plane)) || !self.string(anchor) {
2188                    return Ok(false);
2189                }
2190                let (statement, current) = (&mut self.has_anchor, &self.current);
2191                let lookup =
2192                    || Ok(statement.query_row(params![plane, current, anchor], |row| row.get(0))?);
2193                if plane == 0 {
2194                    cached(&mut self.own_anchors, anchor, lookup)?
2195                } else {
2196                    cached(&mut self.anchors, (plane, anchor), lookup)?
2197                }
2198            }
2199        })
2200    }
2201
2202    fn check(&mut self, object: &ObjectRecord) -> Result<(), Error> {
2203        self.uid(object)?;
2204        let mut ok = self.proto(object.proto)?
2205            && usize::from(object.layer) <= self.render_layers
2206            && self.portal(object.portal.as_ref())?;
2207        // Field names are field-name strings, each at most once per object.
2208        let mut names = BTreeSet::new();
2209        for (name, value) in &object.fields {
2210            ok &= names.insert(*name) && self.field_name(*name)?;
2211            match value {
2212                FieldRecord::String(id) => ok &= self.string(*id),
2213                FieldRecord::Integer(number) => {
2214                    ok &= (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(number);
2215                }
2216                _ => {}
2217            }
2218        }
2219        if ok {
2220            Ok(())
2221        } else {
2222            Err(Error::Rejected(
2223                ErrorCode::Reference,
2224                "an object references a missing string, layer, plane or anchor, repeats or misnames a field, or has a portal position or integer field out of range".into(),
2225            ))
2226        }
2227    }
2228}
2229
2230#[cfg(test)]
2231mod tests {
2232    use super::{CACHE_ENTRIES, Pack, cached};
2233    use sha2::Digest as _;
2234    use std::{collections::BTreeMap, path::Path};
2235
2236    #[test]
2237    fn lookup_caches_stay_bounded() {
2238        let mut cache = BTreeMap::new();
2239        for key in 0..u32::try_from(CACHE_ENTRIES * 3).unwrap() {
2240            assert!(cached(&mut cache, key, || Ok(true)).unwrap());
2241            assert!(cache.len() <= CACHE_ENTRIES);
2242        }
2243        // The latest keys are kept and not looked up again; evicted ones are.
2244        let last = u32::try_from(CACHE_ENTRIES * 3 - 1).unwrap();
2245        let mut again = false;
2246        let kept = cached(&mut cache, last, || {
2247            again = true;
2248            Ok(false)
2249        });
2250        assert!(kept.unwrap() && !again);
2251        let mut queried = false;
2252        let evicted = cached(&mut cache, 0, || {
2253            queried = true;
2254            Ok(false)
2255        });
2256        assert!(!evicted.unwrap() && queried);
2257    }
2258
2259    #[test]
2260    fn an_image_digest_is_of_sqlite_copy() {
2261        let path = crate::tests::valid_pack();
2262        let bytes = std::fs::read(path).unwrap();
2263        let mut pack = Pack::open_bytes(&bytes, None, &super::VerifyLimits::default()).unwrap();
2264        let digest = pack.file_digest().unwrap();
2265        // Serializing the image is a view of the buffer SQLite reads, not a
2266        // second image, and that buffer is what the digest was taken of.
2267        let image = pack.connection.serialize(rusqlite::MAIN_DB).unwrap();
2268        assert!(matches!(image, rusqlite::serialize::Data::Shared(_)));
2269        assert_eq!(&*image, bytes.as_slice());
2270        assert_eq!(<[u8; 32]>::from(super::Sha256::digest(&*image)), digest);
2271    }
2272
2273    #[test]
2274    fn images_are_read_only() {
2275        let path = crate::tests::valid_pack();
2276        let bytes = std::fs::read(path).unwrap();
2277        let mut pack = Pack::open_bytes(&bytes, None, &super::VerifyLimits::default()).unwrap();
2278        // Even with query_only turned off, the image cannot be written.
2279        pack.connection
2280            .execute_batch("PRAGMA query_only = OFF")
2281            .unwrap();
2282        for write in [
2283            "DELETE FROM strings",
2284            "CREATE TABLE extra (value)",
2285            "PRAGMA user_version = 3",
2286        ] {
2287            assert!(pack.connection.execute_batch(write).is_err(), "{write}");
2288        }
2289        pack.verify().unwrap();
2290        // The digest was taken at open, so only the buffer itself can show
2291        // that it still holds the bytes.
2292        let image = pack.connection.serialize(rusqlite::MAIN_DB).unwrap();
2293        assert_eq!(&*image, bytes.as_slice());
2294    }
2295
2296    /// Objects at any depth and their fields.
2297    fn count(objects: &[crate::ObjectRecord]) -> u64 {
2298        objects
2299            .iter()
2300            .map(|object| {
2301                1 + u64::try_from(object.fields.len()).unwrap() + count(&object.inventory)
2302            })
2303            .sum()
2304    }
2305
2306    /// Objects at any depth.
2307    fn count_objects(objects: &[crate::ObjectRecord]) -> u64 {
2308        objects
2309            .iter()
2310            .map(|object| 1 + count_objects(&object.inventory))
2311            .sum()
2312    }
2313
2314    #[test]
2315    fn chunk_reads_keep_to_their_entry_budget() {
2316        let path = crate::tests::valid_pack();
2317        let pack = Pack::open(&path).unwrap();
2318        let chunk = pack.chunk("overworld", 0, 0, 0).unwrap().unwrap();
2319        let entries = count(&chunk.objects);
2320        assert!(
2321            entries > count_objects(&chunk.objects),
2322            "the chunk has fields"
2323        );
2324        assert_eq!(
2325            pack.chunk_within("overworld", 0, 0, 0, entries).unwrap(),
2326            Some(chunk)
2327        );
2328        // Fields count as well as objects.
2329        let error = pack
2330            .chunk_within("overworld", 0, 0, 0, entries - 1)
2331            .unwrap_err();
2332        assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2333        // A chunk without objects fits any budget, and a missing chunk is
2334        // none.
2335        assert!(
2336            pack.chunk_within("overworld", 1, 0, 0, 0)
2337                .unwrap()
2338                .is_some()
2339        );
2340        assert!(
2341            pack.chunk_within("overworld", 0, 9, 9, 0)
2342                .unwrap()
2343                .is_none()
2344        );
2345        // A read of an object decodes its chunk within the budget too.
2346        let uid = "o.k3vq7zr2mxa5d6wfh4pnyt2cgu";
2347        assert!(pack.object_within(uid, entries).unwrap().is_some());
2348        let error = pack.object_within(uid, entries - 1).unwrap_err();
2349        assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2350    }
2351
2352    #[test]
2353    fn stairwell_reads_keep_to_their_step_budget() {
2354        let path = crate::tests::valid_pack();
2355        let pack = Pack::open(&path).unwrap();
2356        let mut stairwells = Vec::new();
2357        pack.visit_stairwells("overworld", crate::Page::all(), &mut |stairwell| {
2358            stairwells.push(stairwell);
2359            Ok(())
2360        })
2361        .unwrap();
2362        let longest = stairwells
2363            .iter()
2364            .max_by_key(|stairwell| stairwell.steps.len())
2365            .unwrap();
2366        let steps = u64::try_from(longest.steps.len()).unwrap();
2367        assert_eq!(
2368            pack.stairwell_within("overworld", &longest.id, steps)
2369                .unwrap()
2370                .as_ref(),
2371            Some(longest)
2372        );
2373        let error = pack
2374            .stairwell_within("overworld", &longest.id, steps - 1)
2375            .unwrap_err();
2376        assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2377        let mut listed = 0;
2378        pack.visit_stairwells_within("overworld", crate::Page::all(), steps, &mut |_| {
2379            listed += 1;
2380            Ok(())
2381        })
2382        .unwrap();
2383        assert_eq!(listed, stairwells.len());
2384        let error = pack
2385            .visit_stairwells_within("overworld", crate::Page::all(), steps - 1, &mut |_| Ok(()))
2386            .unwrap_err();
2387        assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2388    }
2389
2390    #[test]
2391    fn packs_keep_the_absolute_path_they_opened() {
2392        // Unit tests run in the package directory, so this path is relative.
2393        let target = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target");
2394        std::fs::create_dir_all(&target).unwrap();
2395        let directory = tempfile::tempdir_in(&target).unwrap();
2396        let world = tilewright_source::load(
2397            &Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/basic"),
2398        )
2399        .unwrap();
2400        let written = crate::write_pack(&world, directory.path()).unwrap();
2401        let relative = Path::new("../../target")
2402            .join(directory.path().file_name().unwrap())
2403            .join(written.path.file_name().unwrap());
2404        let pack = Pack::open(&relative).unwrap();
2405        let super::Source::File(path) = &pack.source else {
2406            panic!("a pack opened from a path has a file source");
2407        };
2408        assert!(path.is_absolute(), "{}", path.display());
2409        pack.verify().unwrap();
2410    }
2411}