1use 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
40const PAGE_SIZE: i64 = 4096;
42
43const MAXIMUM_META_BYTES: usize = 1024;
45
46const MAXIMUM_SCHEMA_BYTES: usize = 1024;
48
49pub const MAXIMUM_OBJECT_INDEX: usize = MAXIMUM_RECORD_BYTES - 1;
53
54pub const DEFAULT_MAXIMUM_DECODED_BYTES: u64 = 8 * 1024 * 1024 * 1024;
56
57pub const DEFAULT_MAXIMUM_PACK_BYTES: u64 = 8 * 1024 * 1024 * 1024;
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub struct VerifyLimits {
63 pub maximum_decoded_bytes: u64,
67 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
85fn 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#[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 pub objects: u64,
106 pub object_uids: u64,
108}
109
110#[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#[derive(Clone, Debug, Eq, PartialEq)]
123pub struct StairwellTile {
124 pub stairwell: String,
126 pub step: usize,
128 pub proto: String,
129}
130
131pub struct Pack {
132 connection: Connection,
133 source: Source,
134 maximum_bytes: u64,
137}
138
139enum Source {
142 File(PathBuf),
145 Image(Option<[u8; 32]>),
148 Compiled,
150}
151
152#[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 #[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
186fn bound(bytes: usize) -> i64 {
188 i64::try_from(bytes).unwrap_or(i64::MAX)
189}
190
191fn maximum_document_bytes() -> i64 {
193 bound(Limits::default().max_input_bytes)
194}
195
196fn maximum_string_bytes() -> i64 {
198 bound(Limits::default().max_string_bytes)
199}
200
201fn not_identifier(column: &str) -> String {
205 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
213fn 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
225fn 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
237fn chunk_data(value: &types::Value) -> Result<ChunkData, Error> {
239 chunk_data_with(value, None)
240}
241
242fn 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
257fn 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
265fn 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
274fn 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
282const 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
289type StoredLocation = (
291 Option<String>,
292 Option<i64>,
293 Option<i64>,
294 Option<i64>,
295 Option<i64>,
296);
297
298fn 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
306fn bounded_text(column: &str) -> String {
308 format!(
309 "CASE WHEN typeof({column}) = 'text' AND octet_length({column}) <= ?1 THEN {column} END"
310 )
311}
312
313fn 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 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
344fn 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
371fn 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
394fn 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
493fn text<'d>(document: &'d Document, key: &str) -> Option<&'d str> {
495 document.root().get(key).and_then(Value::as_str)
496}
497
498fn 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
514pub(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 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 pub fn open(path: &Path) -> Result<Self, Error> {
558 Self::open_with(path, &VerifyLimits::default())
559 }
560
561 pub fn open_with(path: &Path, limits: &VerifyLimits) -> Result<Self, Error> {
569 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 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 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 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 if size > 0 {
660 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 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 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 pub(crate) fn compiled(connection: Connection) -> Result<Self, Error> {
697 check_identity(&connection)?;
698 Ok(Self {
699 connection,
700 source: Source::Compiled,
701 maximum_bytes: u64::MAX,
703 })
704 }
705
706 pub(crate) fn into_connection(self) -> Connection {
707 self.connection
708 }
709
710 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 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 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 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 pub fn layer_height(&self) -> Result<Option<u32>, Error> {
833 Ok(self.profile_layers()?.2)
834 }
835
836 pub fn stairwell(&self, plane: &str, id: &str) -> Result<Option<Stairwell>, Error> {
845 self.stairwell_within(plane, id, u64::MAX)
846 }
847
848 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 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 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 pub fn object(&self, uid: &str) -> Result<Option<(UidLocation, ObjectRecord)>, Error> {
981 self.object_within(uid, u64::MAX)
982 }
983
984 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 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 pub fn file_digest(&self) -> Result<[u8; 32], Error> {
1062 self.digest_within(self.maximum_bytes)
1063 }
1064
1065 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1697struct Filled<R> {
1702 inner: R,
1703 missing: bool,
1704 failure: Option<std::io::Error>,
1705 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
1731const HEADER_BYTES: usize = 20;
1733
1734fn 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
1754fn 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
1765fn 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
1774fn 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
1795fn check_identity(connection: &Connection) -> Result<(), Error> {
1798 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 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
1880fn 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 let value = utf8_column(row, 1, ErrorCode::Meta)?.unwrap_or_default();
1910 meta.push((row.get(0)?, value));
1911 }
1912 Ok(meta)
1913}
1914
1915struct Targets<'c> {
1921 strings: u32,
1922 render_layers: usize,
1923 is_plane: Statement<'c>,
1925 is_proto: Statement<'c>,
1927 is_zone: Statement<'c>,
1929 has_anchor: Statement<'c>,
1932 value: Statement<'c>,
1934 planes: BTreeMap<u32, bool>,
1935 protos: BTreeMap<u32, bool>,
1936 field_names: BTreeMap<u32, bool>,
1937 anchors: BTreeMap<(u32, u32), bool>,
1939 current: String,
1942 zones: BTreeMap<u32, bool>,
1943 own_anchors: BTreeMap<u32, bool>,
1944 uid_row: Statement<'c>,
1946 chunk: (i32, i64, i64),
1950 index: i64,
1951 uids: u64,
1952}
1953
1954impl<'c> Targets<'c> {
1955 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
1992const CACHE_ENTRIES: usize = 1 << 16;
1996
1997fn 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 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 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 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 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 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 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 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 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 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 let error = pack
2306 .chunk_within("overworld", 0, 0, 0, entries - 1)
2307 .unwrap_err();
2308 assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2309 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 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 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}