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