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 {
149 digest: [u8; 32],
150 expected: Option<[u8; 32]>,
151 },
152 Compiled,
154}
155
156#[cfg_attr(
159 unix,
160 expect(
161 clippy::unnecessary_wraps,
162 reason = "only platforms whose paths must be UTF-8 can fail"
163 )
164)]
165fn uri(absolute: &Path) -> Result<String, Error> {
166 #[cfg(unix)]
167 let bytes = std::os::unix::ffi::OsStrExt::as_bytes(absolute.as_os_str());
168 #[cfg(not(unix))]
169 let bytes = absolute
170 .to_str()
171 .ok_or_else(|| Error::Rejected(ErrorCode::Io, "pack path is not UTF-8".into()))?
172 .as_bytes();
173 #[cfg(unix)]
176 let mut encoded = String::from("file://");
177 #[cfg(not(unix))]
178 let mut encoded = String::from("file:");
179 for byte in bytes {
180 if byte.is_ascii_alphanumeric() || b"/-_.~".contains(byte) {
181 encoded.push(char::from(*byte));
182 } else {
183 let _ = write!(encoded, "%{byte:02X}");
184 }
185 }
186 encoded.push_str("?immutable=1");
187 Ok(encoded)
188}
189
190fn bound(bytes: usize) -> i64 {
192 i64::try_from(bytes).unwrap_or(i64::MAX)
193}
194
195fn maximum_document_bytes() -> i64 {
197 bound(Limits::default().max_input_bytes)
198}
199
200fn maximum_string_bytes() -> i64 {
202 bound(Limits::default().max_string_bytes)
203}
204
205fn not_identifier(column: &str) -> String {
209 format!(
211 "(typeof({column}) != 'text' OR octet_length({column}) NOT BETWEEN 1 AND 128 \
212 OR instr({column}, char(0)) != 0 \
213 OR {column} NOT GLOB '[a-z0-9]*' OR {column} GLOB '*[^a-z0-9_.-]*')"
214 )
215}
216
217fn chunk_data_sql() -> String {
221 format!(
222 "CASE WHEN octet_length(c.encoding) = {length} AND c.encoding = '{CHUNK_ENCODING}' \
223 THEN CASE WHEN typeof(c.data) = 'blob' AND octet_length(c.data) <= {MAXIMUM_CHUNK_BYTES} \
224 THEN c.data ELSE 1 END ELSE 0 END",
225 length = CHUNK_ENCODING.len()
226 )
227}
228
229fn recorded_size(value: &types::Value) -> u64 {
232 match value {
233 types::Value::Blob(data) => zstd::zstd_safe::get_frame_content_size(data)
234 .ok()
235 .flatten()
236 .unwrap_or(0),
237 _ => 0,
238 }
239}
240
241fn chunk_data(value: &types::Value) -> Result<ChunkData, Error> {
243 chunk_data_with(value, None)
244}
245
246fn chunk_data_with(value: &types::Value, visit: Option<Visit<'_>>) -> Result<ChunkData, Error> {
248 match value {
249 types::Value::Blob(data) => decode_chunk_with(data, visit).map_err(Error::Chunk),
250 types::Value::Integer(0) => Err(Error::Rejected(
251 ErrorCode::ChunkEncoding,
252 format!("a chunk's encoding is not {CHUNK_ENCODING}"),
253 )),
254 _ => Err(Error::Rejected(
255 ErrorCode::ChunkData,
256 format!("a chunk's data is not a blob of at most {MAXIMUM_CHUNK_BYTES} bytes"),
257 )),
258 }
259}
260
261fn bounded_identifier(column: &str) -> String {
264 format!(
265 "CASE WHEN typeof({column}) = 'text' AND octet_length({column}) BETWEEN 1 AND 128 THEN {column} END"
266 )
267}
268
269fn step_index(column: &str) -> String {
272 format!(
273 "CASE WHEN typeof({column}) = 'integer' AND {column} BETWEEN 0 AND {} THEN {column} END",
274 Limits::default().max_array_items - 1
275 )
276}
277
278fn bounded_integer(column: &str, minimum: i64, maximum: i64) -> String {
281 format!(
282 "CASE WHEN typeof({column}) = 'integer' AND {column} BETWEEN {minimum} AND {maximum} THEN {column} END"
283 )
284}
285
286const CHUNKS: (i64, i64) = (
289 -(MAX_SAFE_INTEGER / CHUNK_SIZE as i64),
290 (MAX_SAFE_INTEGER - (CHUNK_SIZE as i64 - 1)) / CHUNK_SIZE as i64,
291);
292
293type StoredLocation = (
295 Option<String>,
296 Option<i64>,
297 Option<i64>,
298 Option<i64>,
299 Option<i64>,
300);
301
302fn bad_uid_row() -> Error {
304 Error::Rejected(
305 ErrorCode::ObjectUid,
306 "an object_uids row's plane is not an identifier, or its layer, chunk or index is out of range".into(),
307 )
308}
309
310fn bounded_text(column: &str) -> String {
312 format!(
313 "CASE WHEN typeof({column}) = 'text' AND octet_length({column}) <= ?1 THEN {column} END"
314 )
315}
316
317fn stored_document(text: Option<String>, what: &str, code: ErrorCode) -> Result<Document, Error> {
320 let invalid = || {
321 Error::Rejected(
322 code,
323 format!("{what} is not canonical JSON of an object with namespaced custom data"),
324 )
325 };
326 let text = text.ok_or_else(invalid)?;
327 if !scan::compact(&text) {
328 return Err(invalid());
329 }
330 let document = Document::parse(text, &crate::canonical_limits()).map_err(|_| invalid())?;
331 let custom_ok = match document.root().get("custom") {
334 None => true,
335 Some(custom) => custom
336 .as_object()
337 .is_some_and(|members| members.iter().all(|member| is_namespace(&member.key))),
338 };
339 if document.root().as_object().is_none()
340 || document.canonical(document.root()) != document.source()
341 || !custom_ok
342 {
343 return Err(invalid());
344 }
345 Ok(document)
346}
347
348fn projection_ok(profile: &Document) -> bool {
352 let Some(members) = profile.root().get("projection").and_then(Value::as_object) else {
353 return false;
354 };
355 let keys: BTreeSet<&str> = members.iter().map(|member| member.key.as_str()).collect();
356 let sizes: &[&str] = match members
357 .iter()
358 .find(|member| member.key == "kind")
359 .and_then(|member| member.value.as_str())
360 {
361 Some("isometric") => &["tile_width", "tile_height"],
362 Some("top_down") => &["tile_size"],
363 _ => return false,
364 };
365 keys.len() == sizes.len() + 1
366 && sizes.iter().all(|size| {
367 members
368 .iter()
369 .find(|member| member.key == *size)
370 .and_then(|member| member.value.as_i64())
371 .is_some_and(|value| (1..=i64::from(u32::MAX)).contains(&value))
372 })
373}
374
375fn stored_layer_height(profile: &Document) -> Result<Option<u32>, Error> {
378 profile
379 .root()
380 .get("layer_height")
381 .map(|value| {
382 value
383 .as_i64()
384 .filter(|height| (1..=i64::from(MAXIMUM_LAYER_HEIGHT)).contains(height))
385 .and_then(|height| u32::try_from(height).ok())
386 .ok_or_else(|| {
387 Error::Rejected(
388 ErrorCode::Profile,
389 format!(
390 "the stored profile's layer_height is not an integer from 1 to {MAXIMUM_LAYER_HEIGHT}"
391 ),
392 )
393 })
394 })
395 .transpose()
396}
397
398fn stored_stairwell(document: &Document) -> Result<Stairwell, Error> {
404 let invalid = || {
405 Error::Rejected(
406 ErrorCode::Stairwell,
407 "a stairwell document does not have the source format's shape".into(),
408 )
409 };
410 let closed = |value: &Value, keys: &[&str]| {
411 value.as_object().is_some_and(|members| {
412 members
413 .iter()
414 .all(|member| keys.contains(&member.key.as_str()))
415 })
416 };
417 let root = document.root();
418 if !closed(
419 root,
420 &[
421 "id",
422 "steps",
423 "direction",
424 "hidden_on_other_layers",
425 "custom",
426 ],
427 ) {
428 return Err(invalid());
429 }
430 let id = text(document, "id")
431 .filter(|id| is_identifier(id))
432 .ok_or_else(invalid)?;
433 let direction = match root.get("direction") {
434 None => StairwellDirection::Both,
435 Some(value) => value
436 .as_str()
437 .and_then(StairwellDirection::parse)
438 .ok_or_else(invalid)?,
439 };
440 let hidden_on_other_layers = match root.get("hidden_on_other_layers") {
441 None => true,
442 Some(value) => value.as_bool().ok_or_else(invalid)?,
443 };
444 let in_range = |value: &Value| {
445 value
446 .as_i64()
447 .filter(|number| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(number))
448 };
449 let mut steps = Vec::new();
450 for step in root
451 .get("steps")
452 .and_then(Value::as_array)
453 .ok_or_else(invalid)?
454 {
455 if !closed(step, &["layer", "at", "proto"]) {
456 return Err(invalid());
457 }
458 let layer = step
459 .get("layer")
460 .and_then(Value::as_i64)
461 .and_then(|layer| i32::try_from(layer).ok());
462 let at = match step.get("at").and_then(Value::as_array) {
463 Some([x, y]) => in_range(x).zip(in_range(y)).map(|(x, y)| [x, y]),
464 _ => None,
465 };
466 let proto = step
467 .get("proto")
468 .and_then(Value::as_str)
469 .filter(|proto| is_identifier(proto));
470 let (Some(layer), Some(at), Some(proto)) = (layer, at, proto) else {
471 return Err(invalid());
472 };
473 steps.push(StairwellStep {
474 layer,
475 at,
476 proto: proto.to_owned(),
477 });
478 }
479 if !Stairwell::connects(&steps) {
480 return Err(Error::Rejected(
481 ErrorCode::Stairwell,
482 format!(
483 "the steps of stairwell {id} are not on adjacent layers in one direction, at most one tile apart"
484 ),
485 ));
486 }
487 Ok(Stairwell {
488 id: id.to_owned(),
489 steps,
490 direction,
491 hidden_on_other_layers,
492 custom: root.get("custom").map(|custom| document.canonical(custom)),
493 canonical: document.source().to_owned(),
494 })
495}
496
497fn text<'d>(document: &'d Document, key: &str) -> Option<&'d str> {
499 document.root().get(key).and_then(Value::as_str)
500}
501
502fn utf8_column(
506 row: &rusqlite::Row<'_>,
507 index: usize,
508 code: ErrorCode,
509) -> Result<Option<String>, Error> {
510 match row.get_ref(index)? {
511 types::ValueRef::Text(bytes) => std::str::from_utf8(bytes)
512 .map(|text| Some(text.to_owned()))
513 .map_err(|_| Error::Rejected(code, "a stored value is not UTF-8 text".into())),
514 _ => Ok(None),
515 }
516}
517
518pub(super) fn stairwell_row_within(
524 row: &rusqlite::Row<'_>,
525 maximum_steps: u64,
526) -> Result<Stairwell, Error> {
527 let id = utf8_column(row, 0, ErrorCode::Stairwell)?;
528 let text = utf8_column(row, 1, ErrorCode::Stairwell)?;
529 let steps = text
532 .as_deref()
533 .filter(|text| scan::compact(text))
534 .and_then(|text| scan::root_array_items(text, "steps"));
535 if steps.is_some_and(|steps| u64::try_from(steps).unwrap_or(u64::MAX) > maximum_steps) {
536 return Err(Error::Rejected(
537 ErrorCode::DecodeBudget,
538 format!("a stairwell lists more than {maximum_steps} steps, the read's budget"),
539 ));
540 }
541 let document = stored_document(text, "a stairwell document", ErrorCode::Stairwell)?;
542 let stairwell = stored_stairwell(&document)?;
543 if id.as_deref() != Some(stairwell.id.as_str()) {
544 return Err(Error::Rejected(
545 ErrorCode::Stairwell,
546 format!(
547 "the document of stairwell {} disagrees with its row",
548 stairwell.id
549 ),
550 ));
551 }
552 Ok(stairwell)
553}
554
555impl Pack {
556 pub fn open(path: &Path) -> Result<Self, Error> {
562 Self::open_with(path, &VerifyLimits::default())
563 }
564
565 pub fn open_with(path: &Path, limits: &VerifyLimits) -> Result<Self, Error> {
573 let path = std::path::absolute(path)?;
576 let metadata = std::fs::metadata(&path)?;
577 if !metadata.is_file() {
578 return Err(Error::Rejected(
579 ErrorCode::NotAFile,
580 format!("{} is not a file", path.display()),
581 ));
582 }
583 if metadata.len() > limits.maximum_pack_bytes {
584 return Err(too_long(limits.maximum_pack_bytes));
585 }
586 let mut head = Vec::with_capacity(HEADER_BYTES);
587 std::fs::File::open(&path)?
588 .take(HEADER_BYTES as u64)
589 .read_to_end(&mut head)?;
590 check_header(&head)?;
591 let connection = Connection::open_with_flags(
592 uri(&path)?,
593 OpenFlags::SQLITE_OPEN_READ_ONLY
594 | OpenFlags::SQLITE_OPEN_URI
595 | OpenFlags::SQLITE_OPEN_NO_MUTEX,
596 )?;
597 harden(&connection)?;
598 check_identity(&connection)?;
599 check_pages(&connection, std::fs::metadata(&path)?.len(), limits)?;
602 Ok(Self {
603 connection,
604 source: Source::File(path),
605 maximum_bytes: limits.maximum_pack_bytes,
606 })
607 }
608
609 pub fn open_bytes(
615 bytes: &[u8],
616 digest: Option<[u8; 32]>,
617 limits: &VerifyLimits,
618 ) -> Result<Self, Error> {
619 let length = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
620 Self::open_from(bytes, length, digest, limits)
621 }
622
623 pub fn open_from(
639 read: impl std::io::Read,
640 length: u64,
641 digest: Option<[u8; 32]>,
642 limits: &VerifyLimits,
643 ) -> Result<Self, Error> {
644 if length > limits.maximum_pack_bytes {
645 return Err(too_long(limits.maximum_pack_bytes));
646 }
647 let size = usize::try_from(length).map_err(|_| {
648 Error::Io(std::io::Error::new(
649 std::io::ErrorKind::OutOfMemory,
650 "the pack is larger than memory can hold",
651 ))
652 })?;
653 let mut connection = Connection::open_in_memory_with_flags(
654 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
655 )?;
656 let mut image = Filled {
657 inner: read,
658 missing: false,
659 failure: None,
660 head: Vec::with_capacity(HEADER_BYTES),
661 hasher: Sha256::new(),
662 };
663 if size > 0 {
666 connection.deserialize_read_exact(rusqlite::MAIN_DB, &mut image, size, true)?;
670 }
671 if let Some(error) = image.failure {
672 return Err(Error::Io(error));
673 }
674 if image.missing {
675 return Err(Error::Io(std::io::Error::new(
676 std::io::ErrorKind::UnexpectedEof,
677 format!("the pack image ends before its {length} bytes"),
678 )));
679 }
680 if read_some(&mut image.inner)? {
683 return Err(Error::Rejected(
684 ErrorCode::FileLength,
685 format!("the pack image is longer than {length} bytes"),
686 ));
687 }
688 check_header(&image.head)?;
691 harden(&connection)?;
692 check_identity(&connection)?;
693 check_pages(&connection, length, limits)?;
694 Ok(Self {
695 connection,
696 source: Source::Image {
697 digest: image.hasher.finalize().into(),
698 expected: digest,
699 },
700 maximum_bytes: limits.maximum_pack_bytes,
701 })
702 }
703
704 pub(crate) fn compiled(connection: Connection) -> Result<Self, Error> {
706 check_identity(&connection)?;
707 Ok(Self {
708 connection,
709 source: Source::Compiled,
710 maximum_bytes: u64::MAX,
712 })
713 }
714
715 pub(crate) fn into_connection(self) -> Connection {
716 self.connection
717 }
718
719 pub fn meta(&self, key: &str) -> Result<Option<String>, Error> {
724 let mut statement = self
725 .connection
726 .prepare("SELECT value FROM meta WHERE key = ?1")?;
727 let mut rows = statement.query([key])?;
728 match rows.next()? {
729 Some(row) => utf8_column(row, 0, ErrorCode::Meta),
730 None => Ok(None),
731 }
732 }
733
734 pub fn string(&self, id: u32) -> Result<Option<String>, Error> {
740 let mut statement = self.connection.prepare(&format!(
741 "SELECT {} FROM strings WHERE id = ?2",
742 bounded_text("value")
743 ))?;
744 let mut rows = statement.query(params![maximum_string_bytes(), id])?;
745 let Some(row) = rows.next()? else {
746 return Ok(None);
747 };
748 match utf8_column(row, 0, ErrorCode::String)? {
749 Some(text) => Ok(Some(text)),
750 None => Err(Error::Rejected(
751 ErrorCode::String,
752 "a string is not text of at most 1 MiB".into(),
753 )),
754 }
755 }
756
757 pub fn chunk(
764 &self,
765 plane: &str,
766 layer: i32,
767 cx: i64,
768 cy: i64,
769 ) -> Result<Option<ChunkData>, Error> {
770 self.chunk_within(plane, layer, cx, cy, u64::MAX)
771 }
772
773 pub fn chunk_within(
785 &self,
786 plane: &str,
787 layer: i32,
788 cx: i64,
789 cy: i64,
790 maximum_entries: u64,
791 ) -> Result<Option<ChunkData>, Error> {
792 let value: Option<types::Value> = self
793 .connection
794 .query_row(
795 &format!(
796 "SELECT {} FROM chunks c WHERE c.plane = ?1 AND c.layer = ?2 AND c.cx = ?3 AND c.cy = ?4",
797 chunk_data_sql()
798 ),
799 params![plane, layer, cx, cy],
800 |row| row.get(0),
801 )
802 .optional()?;
803 let Some(value) = value else {
804 return Ok(None);
805 };
806 if maximum_entries < u64::MAX {
807 let mut entries: u64 = 0;
808 let mut over = false;
809 let mut count = |object: &ObjectRecord| {
810 let fields = u64::try_from(object.fields.len()).unwrap_or(u64::MAX);
811 entries = entries.saturating_add(1).saturating_add(fields);
812 over = entries > maximum_entries;
813 if over {
814 Err(DecodeError(
815 "the chunk holds more entries than the budget".into(),
816 ))
817 } else {
818 Ok(())
819 }
820 };
821 let counted = chunk_data_with(&value, Some(&mut count));
822 if over {
823 return Err(Error::Rejected(
824 ErrorCode::DecodeBudget,
825 format!(
826 "the chunk holds more than {maximum_entries} objects and fields, the read's budget"
827 ),
828 ));
829 }
830 counted?;
831 }
832 chunk_data(&value).map(Some)
833 }
834
835 pub fn layer_height(&self) -> Result<Option<u32>, Error> {
842 Ok(self.profile_layers()?.2)
843 }
844
845 pub fn stairwell(&self, plane: &str, id: &str) -> Result<Option<Stairwell>, Error> {
854 self.stairwell_within(plane, id, u64::MAX)
855 }
856
857 pub fn stairwell_within(
866 &self,
867 plane: &str,
868 id: &str,
869 maximum_steps: u64,
870 ) -> Result<Option<Stairwell>, Error> {
871 let mut statement = self.connection.prepare(&format!(
872 "SELECT {}, {} FROM stairwells WHERE plane = ?2 AND id = ?3",
873 bounded_identifier("id"),
874 bounded_text("document"),
875 ))?;
876 let mut rows = statement.query(params![maximum_document_bytes(), plane, id])?;
877 rows.next()?
878 .map(|row| stairwell_row_within(row, maximum_steps))
879 .transpose()
880 }
881
882 pub fn stairwell_step(
889 &self,
890 plane: &str,
891 layer: i32,
892 x: i64,
893 y: i64,
894 ) -> Result<Option<StairwellTile>, Error> {
895 let mut statement = self.connection.prepare(&format!(
896 "SELECT {}, {}, {} FROM stairwell_steps \
897 WHERE plane = ?1 AND layer = ?2 AND x = ?3 AND y = ?4",
898 bounded_identifier("stairwell"),
899 step_index("step"),
900 bounded_identifier("proto"),
901 ))?;
902 let mut rows = statement.query(params![plane, layer, x, y])?;
903 let Some(row) = rows.next()? else {
904 return Ok(None);
905 };
906 let found: (Option<String>, Option<i64>, Option<String>) = (
907 utf8_column(row, 0, ErrorCode::StairwellStep)?,
908 row.get(1)?,
909 utf8_column(row, 2, ErrorCode::StairwellStep)?,
910 );
911 match found {
912 (Some(stairwell), Some(step), Some(proto))
913 if is_identifier(&stairwell) && is_identifier(&proto) =>
914 {
915 Ok(Some(StairwellTile {
916 stairwell,
917 step: usize::try_from(step).map_err(|_| {
918 Error::Rejected(
919 ErrorCode::StairwellStep,
920 "a stairwell step has a negative index".into(),
921 )
922 })?,
923 proto,
924 }))
925 }
926 _ => Err(Error::Rejected(
927 ErrorCode::StairwellStep,
928 "a stairwell step's stairwell or proto is not an identifier, or its index is not one a source array can have".into(),
929 )),
930 }
931 }
932
933 pub fn object_location(&self, uid: &str) -> Result<Option<UidLocation>, Error> {
941 if !is_identifier(uid) {
942 return Ok(None);
943 }
944 let mut statement = self.connection.prepare(&format!(
945 "SELECT {}, {}, {}, {}, {} FROM object_uids WHERE uid = ?1",
946 bounded_identifier("plane"),
947 bounded_integer("layer", i64::from(i32::MIN), i64::from(i32::MAX)),
948 bounded_integer("cx", CHUNKS.0, CHUNKS.1),
949 bounded_integer("cy", CHUNKS.0, CHUNKS.1),
950 bounded_integer("object", 0, bound(MAXIMUM_OBJECT_INDEX)),
951 ))?;
952 let mut rows = statement.query([uid])?;
953 let found: Option<StoredLocation> = match rows.next()? {
954 Some(row) => Some((
955 utf8_column(row, 0, ErrorCode::ObjectUid)?,
956 row.get(1)?,
957 row.get(2)?,
958 row.get(3)?,
959 row.get(4)?,
960 )),
961 None => None,
962 };
963 let Some(found) = found else {
964 return Ok(None);
965 };
966 match found {
967 (Some(plane), Some(layer), Some(cx), Some(cy), Some(index))
968 if is_identifier(&plane) =>
969 {
970 Ok(Some(UidLocation {
971 plane,
972 layer: i32::try_from(layer).map_err(|_| bad_uid_row())?,
973 cx,
974 cy,
975 index: usize::try_from(index).map_err(|_| bad_uid_row())?,
976 }))
977 }
978 _ => Err(bad_uid_row()),
979 }
980 }
981
982 pub fn object(&self, uid: &str) -> Result<Option<(UidLocation, ObjectRecord)>, Error> {
990 self.object_within(uid, u64::MAX)
991 }
992
993 pub fn object_within(
1000 &self,
1001 uid: &str,
1002 maximum_entries: u64,
1003 ) -> Result<Option<(UidLocation, ObjectRecord)>, Error> {
1004 let Some(location) = self.object_location(uid)? else {
1005 return Ok(None);
1006 };
1007 let object = self
1008 .chunk_within(
1009 &location.plane,
1010 location.layer,
1011 location.cx,
1012 location.cy,
1013 maximum_entries,
1014 )?
1015 .and_then(|chunk| {
1016 chunk
1017 .object(location.index)
1018 .filter(|object| object.uid.as_deref() == Some(uid))
1019 .cloned()
1020 })
1021 .ok_or_else(|| {
1022 Error::Rejected(
1023 ErrorCode::ObjectUid,
1024 format!("the object_uids row of {uid} does not name an object with that uid"),
1025 )
1026 })?;
1027 Ok(Some((location, object)))
1028 }
1029
1030 pub fn summary(&self) -> Result<Summary, Error> {
1035 let count = |table: &str| -> Result<u64, Error> {
1036 let value: i64 =
1037 self.connection
1038 .query_row(&format!("SELECT count(*) FROM {table}"), [], |row| {
1039 row.get(0)
1040 })?;
1041 Ok(u64::try_from(value).unwrap_or(0))
1042 };
1043 Ok(Summary {
1044 strings: count("strings")?,
1045 planes: count("planes")?,
1046 zones: count("zones")?,
1047 anchors: count("anchors")?,
1048 stairwells: count("stairwells")?,
1049 chunks: count("chunks")?,
1050 definitions: count("definitions")?,
1051 objects: 0,
1052 object_uids: count("object_uids")?,
1053 })
1054 }
1055
1056 pub fn file_digest(&self) -> Result<[u8; 32], Error> {
1071 self.digest_within(self.maximum_bytes)
1072 }
1073
1074 fn digest_within(&self, maximum: u64) -> Result<[u8; 32], Error> {
1076 let path = match &self.source {
1077 Source::File(path) => path,
1078 Source::Image { digest, .. } => return Ok(*digest),
1079 Source::Compiled => {
1080 return Err(Error::Rejected(
1081 ErrorCode::NotAFile,
1082 "the pack is not a file".into(),
1083 ));
1084 }
1085 };
1086 let file = std::fs::File::open(path)?;
1087 if file.metadata()?.len() > maximum {
1088 return Err(too_long(maximum));
1089 }
1090 let mut file = file.take(maximum.saturating_add(1));
1093 let mut hasher = Sha256::new();
1094 let mut buffer = vec![0; 64 * 1024];
1095 let mut total: u64 = 0;
1096 loop {
1097 let read = file.read(&mut buffer)?;
1098 if read == 0 {
1099 break;
1100 }
1101 total = total.saturating_add(u64::try_from(read).unwrap_or(u64::MAX));
1102 if total > maximum {
1103 return Err(too_long(maximum));
1104 }
1105 hasher.update(&buffer[..read]);
1106 }
1107 Ok(hasher.finalize().into())
1108 }
1109
1110 pub fn verify(&self) -> Result<Summary, Error> {
1116 self.verify_with(&VerifyLimits {
1117 maximum_pack_bytes: self.maximum_bytes,
1118 ..VerifyLimits::default()
1119 })
1120 }
1121
1122 pub fn verify_with(&self, limits: &VerifyLimits) -> Result<Summary, Error> {
1143 let maximum_bytes = limits.maximum_pack_bytes.min(self.maximum_bytes);
1144 let pages: i64 = self
1145 .connection
1146 .query_row("PRAGMA page_count", [], |row| row.get(0))?;
1147 if u64::try_from(pages)
1148 .ok()
1149 .and_then(|pages| pages.checked_mul(PAGE_SIZE.unsigned_abs()))
1150 .is_none_or(|bytes| bytes > maximum_bytes)
1151 {
1152 return Err(too_long(maximum_bytes));
1153 }
1154 let hashed = matches!(self.source, Source::Image { .. });
1157 if hashed {
1158 self.check_digest(maximum_bytes)?;
1159 }
1160 let integrity: String = self
1161 .connection
1162 .query_row("PRAGMA integrity_check", [], |row| row.get(0))?;
1163 if integrity != "ok" {
1164 return Err(Error::Rejected(
1165 ErrorCode::Integrity,
1166 format!("integrity check failed: {integrity}"),
1167 ));
1168 }
1169 if !hashed {
1170 self.check_digest(maximum_bytes)?;
1171 }
1172 self.verify_contents(limits.maximum_decoded_bytes)
1173 }
1174
1175 fn check_digest(&self, maximum_bytes: u64) -> Result<(), Error> {
1178 let Some(expected) = self.expected_digest() else {
1179 return Ok(());
1180 };
1181 let digest = crate::compile::hex(&self.digest_within(maximum_bytes)?);
1183 if digest != expected {
1184 return Err(Error::Rejected(
1185 ErrorCode::Digest,
1186 match self.source {
1187 Source::File(_) => format!("file digest {digest} does not match its name"),
1188 _ => format!("pack digest {digest} is not {expected}, the expected digest"),
1189 },
1190 ));
1191 }
1192 Ok(())
1193 }
1194
1195 fn expected_digest(&self) -> Option<String> {
1198 match &self.source {
1199 Source::File(path) => path
1200 .file_name()
1201 .and_then(|name| name.to_str())
1202 .and_then(|name| name.strip_suffix(".twpack"))
1203 .filter(|stem| {
1204 stem.len() == 64 && stem.bytes().all(|byte| byte.is_ascii_hexdigit())
1205 })
1206 .map(str::to_owned),
1207 Source::Image { expected, .. } => {
1208 expected.as_ref().map(|digest| crate::compile::hex(digest))
1209 }
1210 Source::Compiled => None,
1211 }
1212 }
1213
1214 pub(crate) fn verify_contents(&self, maximum_decoded_bytes: u64) -> Result<Summary, Error> {
1217 let mut summary = self.summary()?;
1218 let (minimum, maximum): (Option<i64>, Option<i64>) =
1219 self.connection
1220 .query_row("SELECT min(id), max(id) FROM strings", [], |row| {
1221 Ok((row.get(0)?, row.get(1)?))
1222 })?;
1223 let contiguous = match (minimum, maximum) {
1224 (None, None) => summary.strings == 0,
1225 (Some(1), Some(maximum)) => u64::try_from(maximum).ok() == Some(summary.strings),
1226 _ => false,
1227 };
1228 if !contiguous {
1229 return Err(Error::Rejected(
1230 ErrorCode::StringIds,
1231 "string IDs are not 1..=count".into(),
1232 ));
1233 }
1234 self.check_rows()?;
1235 self.check_zone_trees()?;
1236 self.check_string_encoding()?;
1237 self.check_documents()?;
1238 self.check_stairwells()?;
1239 let (render_layers, ground_layers, _) = self.profile_layers()?;
1240 let strings = u32::try_from(summary.strings)
1241 .map_err(|_| Error::Rejected(ErrorCode::StringIds, "too many strings".into()))?;
1242 let mut targets = Targets::new(&self.connection, strings, render_layers)?;
1243 let mut statement = self.connection.prepare(&format!(
1244 "SELECT p.id, p.chunk_size, c.layer, c.cx, c.cy, {} FROM chunks c JOIN planes p ON p.id = c.plane ORDER BY c.plane, c.layer, c.cx, c.cy",
1245 chunk_data_sql()
1246 ))?;
1247 let mut rows = statement.query([])?;
1248 let mut checked = 0;
1249 let mut decoded: u64 = 0;
1250 while let Some(row) = rows.next()? {
1251 let plane: String = row.get(0)?;
1252 let chunk_size: u16 = row.get(1)?;
1253 let key: (i32, i64, i64) = (row.get(2)?, row.get(3)?, row.get(4)?);
1254 let data: types::Value = row.get(5)?;
1255 decoded = decoded.saturating_add(recorded_size(&data));
1258 if decoded > maximum_decoded_bytes {
1259 return Err(Error::Rejected(
1260 ErrorCode::DecodeBudget,
1261 format!(
1262 "the chunks decode to more than {maximum_decoded_bytes} bytes, the verification limit"
1263 ),
1264 ));
1265 }
1266 targets.enter(&plane, key);
1269 let mut failure = None;
1270 let mut visit = |object: &ObjectRecord| {
1271 summary.objects += 1;
1272 targets.check(object).map_err(|error| {
1273 failure = Some(error);
1274 DecodeError("an object failed verification".into())
1275 })
1276 };
1277 let decoded_chunk = chunk_data_with(&data, Some(&mut visit));
1278 if let Some(error) = failure {
1279 return Err(error);
1280 }
1281 let chunk = decoded_chunk?;
1282 if chunk.chunk_size != chunk_size || chunk.ground.len() != ground_layers {
1283 return Err(Error::Rejected(
1284 ErrorCode::Chunk,
1285 "a chunk disagrees with its plane or profile".into(),
1286 ));
1287 }
1288 targets.check_palette(&chunk)?;
1289 checked += 1;
1290 }
1291 if checked != summary.chunks {
1292 return Err(Error::Rejected(
1293 ErrorCode::Chunk,
1294 "a chunk names an unknown plane".into(),
1295 ));
1296 }
1297 if targets.uids != summary.object_uids {
1302 return Err(Error::Rejected(
1303 ErrorCode::ObjectUid,
1304 "object_uids holds a uid that no object has".into(),
1305 ));
1306 }
1307 Ok(summary)
1308 }
1309
1310 fn check_rows(&self) -> Result<(), Error> {
1313 let checks = [
1314 (
1315 format!(
1316 "SELECT count(*) FROM strings WHERE typeof(value) != 'text' OR octet_length(value) > {} OR instr(value, char(0)) != 0",
1317 maximum_string_bytes()
1318 ),
1319 ErrorCode::String,
1320 "a string is not text of at most 1 MiB without NUL",
1321 ),
1322 (
1323 "SELECT count(*) FROM strings a JOIN strings b ON b.id = a.id + 1 WHERE b.value <= a.value".to_owned(),
1324 ErrorCode::StringIds,
1325 "string IDs are not in byte order of their values",
1326 ),
1327 (
1328 format!(
1329 "SELECT count(*) FROM planes WHERE {} OR kind NOT IN ('shared', 'instanced', 'per_player') OR typeof(chunk_size) != 'integer' OR chunk_size != {CHUNK_SIZE}",
1330 not_identifier("id")
1331 ),
1332 ErrorCode::Plane,
1333 "a plane has an invalid ID or kind, or a chunk size other than 32",
1334 ),
1335 (
1336 format!(
1337 "SELECT count(*) FROM zones z WHERE {} OR NOT EXISTS (SELECT 1 FROM planes p WHERE p.id = z.plane) OR (z.parent IS NOT NULL AND NOT EXISTS (SELECT 1 FROM zones q WHERE q.plane = z.plane AND q.id = z.parent))",
1338 not_identifier("z.id")
1339 ),
1340 ErrorCode::Zone,
1341 "a zone has an invalid ID or names an unknown plane or parent",
1342 ),
1343 (
1344 format!(
1345 "SELECT count(*) FROM anchors a WHERE {} OR NOT EXISTS (SELECT 1 FROM planes p WHERE p.id = a.plane) OR typeof(a.layer) != 'integer' OR a.layer NOT BETWEEN -2147483648 AND 2147483647 OR typeof(a.x) != 'integer' OR a.x NOT BETWEEN -{MAX_SAFE_INTEGER} AND {MAX_SAFE_INTEGER} OR typeof(a.y) != 'integer' OR a.y NOT BETWEEN -{MAX_SAFE_INTEGER} AND {MAX_SAFE_INTEGER}",
1346 not_identifier("a.id")
1347 ),
1348 ErrorCode::Anchor,
1349 "an anchor has an invalid ID, layer or position, or names an unknown plane",
1350 ),
1351 (
1352 format!(
1353 "SELECT count(*) FROM stairwells s WHERE {} OR NOT EXISTS (SELECT 1 FROM planes p WHERE p.id = s.plane)",
1354 not_identifier("s.id")
1355 ),
1356 ErrorCode::Stairwell,
1357 "a stairwell has an invalid ID or names an unknown plane",
1358 ),
1359 (
1360 format!(
1363 "SELECT count(*) FROM stairwell_steps t WHERE NOT EXISTS (SELECT 1 FROM stairwells s WHERE s.plane = t.plane AND s.id = t.stairwell) \
1364 OR typeof(t.layer) != 'integer' OR t.layer NOT BETWEEN -2147483648 AND 2147483647 \
1365 OR typeof(t.x) != 'integer' OR t.x NOT BETWEEN -{MAX_SAFE_INTEGER} AND {MAX_SAFE_INTEGER} \
1366 OR typeof(t.y) != 'integer' OR t.y NOT BETWEEN -{MAX_SAFE_INTEGER} AND {MAX_SAFE_INTEGER} \
1367 OR {} IS NULL \
1368 OR {} OR NOT EXISTS (SELECT 1 FROM definitions d WHERE d.type = 'proto' AND d.id = t.proto)",
1369 step_index("t.step"),
1370 not_identifier("t.proto")
1371 ),
1372 ErrorCode::StairwellStep,
1373 "a stairwell step names an unknown stairwell or proto, or has an invalid layer, position or index",
1374 ),
1375 (
1376 format!(
1378 "SELECT count(*) FROM chunks c LEFT JOIN planes p ON p.id = c.plane WHERE p.id IS NULL \
1379 OR typeof(c.layer) != 'integer' OR c.layer NOT BETWEEN -2147483648 AND 2147483647 \
1380 OR typeof(c.cx) != 'integer' OR typeof(c.cy) != 'integer' \
1381 OR c.cx NOT BETWEEN -({MAX_SAFE_INTEGER} / p.chunk_size) AND ({MAX_SAFE_INTEGER} - p.chunk_size + 1) / p.chunk_size \
1382 OR c.cy NOT BETWEEN -({MAX_SAFE_INTEGER} / p.chunk_size) AND ({MAX_SAFE_INTEGER} - p.chunk_size + 1) / p.chunk_size"
1383 ),
1384 ErrorCode::Chunk,
1385 "a chunk names an unknown plane or has an invalid layer or position",
1386 ),
1387 (
1388 format!(
1389 "SELECT count(*) FROM definitions WHERE {} OR {}",
1390 not_identifier("type"),
1391 not_identifier("id")
1392 ),
1393 ErrorCode::Definition,
1394 "a definition type or ID is not an identifier",
1395 ),
1396 (
1397 format!(
1400 "SELECT count(*) FROM object_uids u WHERE {} \
1401 OR typeof(u.layer) != 'integer' OR typeof(u.cx) != 'integer' OR typeof(u.cy) != 'integer' \
1402 OR typeof(u.object) != 'integer' OR u.object NOT BETWEEN 0 AND {MAXIMUM_OBJECT_INDEX} \
1403 OR NOT EXISTS (SELECT 1 FROM chunks c WHERE c.plane = u.plane AND c.layer = u.layer AND c.cx = u.cx AND c.cy = u.cy)",
1404 not_identifier("u.uid")
1405 ),
1406 ErrorCode::ObjectUid,
1407 "an object_uids row has an invalid uid or index, or names a missing chunk",
1408 ),
1409 ];
1410 for (query, code, problem) in checks {
1411 let count: i64 = self.connection.query_row(&query, [], |row| row.get(0))?;
1412 if count != 0 {
1413 return Err(Error::Rejected(code, problem.into()));
1414 }
1415 }
1416 Ok(())
1417 }
1418
1419 fn check_zone_trees(&self) -> Result<(), Error> {
1426 let limit = i64::try_from(Limits::default().max_array_items).unwrap_or(i64::MAX);
1427 let mut planes = self
1428 .connection
1429 .prepare("SELECT id FROM planes ORDER BY id")?;
1430 let mut count = self
1431 .connection
1432 .prepare("SELECT count(*) FROM zones WHERE plane = ?1")?;
1433 let mut unrooted = self.connection.prepare(
1437 "WITH RECURSIVE children(parent, id) AS MATERIALIZED \
1438 (SELECT parent, id FROM zones WHERE plane = ?1 AND parent IS NOT NULL), \
1439 rooted(id) AS (SELECT id FROM zones WHERE plane = ?1 AND parent IS NULL \
1440 UNION ALL SELECT c.id FROM rooted r JOIN children c ON c.parent = r.id) \
1441 SELECT (SELECT count(*) FROM zones WHERE plane = ?1) - (SELECT count(*) FROM rooted)",
1442 )?;
1443 let mut rows = planes.query([])?;
1444 while let Some(row) = rows.next()? {
1445 let plane: String = row.get(0)?;
1446 let zones: i64 = count.query_row([&plane], |row| row.get(0))?;
1447 if zones > limit {
1448 return Err(Error::Rejected(
1449 ErrorCode::Zone,
1450 format!("plane {plane} has more zones than a source document holds"),
1451 ));
1452 }
1453 let missing: i64 = unrooted.query_row([&plane], |row| row.get(0))?;
1454 if missing != 0 {
1455 return Err(Error::Rejected(
1456 ErrorCode::Zone,
1457 "zone parents form a cycle".into(),
1458 ));
1459 }
1460 }
1461 Ok(())
1462 }
1463
1464 fn check_string_encoding(&self) -> Result<(), Error> {
1467 let mut statement = self
1468 .connection
1469 .prepare("SELECT value FROM strings ORDER BY id")?;
1470 let mut rows = statement.query([])?;
1471 while let Some(row) = rows.next()? {
1472 match row.get_ref(0)? {
1473 types::ValueRef::Text(bytes) if std::str::from_utf8(bytes).is_ok() => {}
1474 _ => {
1475 return Err(Error::Rejected(
1476 ErrorCode::String,
1477 "a string is not UTF-8 text".into(),
1478 ));
1479 }
1480 }
1481 }
1482 Ok(())
1483 }
1484
1485 fn check_documents(&self) -> Result<(), Error> {
1488 let limit = maximum_document_bytes();
1489 let document = bounded_text("document");
1490 let mut planes = self.connection.prepare(&format!(
1491 "SELECT id, kind, chunk_size, {document} FROM planes ORDER BY id"
1492 ))?;
1493 let mut has_zone = self
1494 .connection
1495 .prepare("SELECT EXISTS (SELECT 1 FROM zones WHERE plane = ?1 AND id = ?2)")?;
1496 let mut rows = planes.query([limit])?;
1497 while let Some(row) = rows.next()? {
1498 let (id, kind, chunk_size): (String, String, i64) =
1499 (row.get(0)?, row.get(1)?, row.get(2)?);
1500 let stored = stored_document(
1501 utf8_column(row, 3, ErrorCode::Plane)?,
1502 "a plane document",
1503 ErrorCode::Plane,
1504 )?;
1505 let default_zone_ok = match stored.root().get("default_zone") {
1506 None => true,
1507 Some(zone) => match zone.as_str() {
1508 Some(zone) => has_zone.query_row(params![id, zone], |row| row.get(0))?,
1509 None => false,
1510 },
1511 };
1512 if text(&stored, "format") != Some("tilewright.plane/1")
1513 || text(&stored, "id") != Some(&id)
1514 || text(&stored, "kind") != Some(&kind)
1515 || stored.root().get("chunk_size").and_then(Value::as_i64) != Some(chunk_size)
1516 || !default_zone_ok
1517 {
1518 return Err(Error::Rejected(
1519 ErrorCode::Plane,
1520 format!("the document of plane {id} disagrees with its row"),
1521 ));
1522 }
1523 }
1524 let mut zones = self.connection.prepare(&format!(
1525 "SELECT plane, id, parent, {document} FROM zones ORDER BY plane, id"
1526 ))?;
1527 let mut rows = zones.query([limit])?;
1528 while let Some(row) = rows.next()? {
1529 let (plane, id, parent): (String, String, Option<String>) =
1530 (row.get(0)?, row.get(1)?, row.get(2)?);
1531 let stored = stored_document(
1532 utf8_column(row, 3, ErrorCode::Zone)?,
1533 "a zone document",
1534 ErrorCode::Zone,
1535 )?;
1536 if text(&stored, "id") != Some(&id)
1537 || stored.root().get("parent").map(Value::as_str) != parent.as_deref().map(Some)
1538 {
1539 return Err(Error::Rejected(
1540 ErrorCode::Zone,
1541 format!("the document of zone {plane}/{id} disagrees with its row"),
1542 ));
1543 }
1544 }
1545 let mut definitions = self.connection.prepare(&format!(
1546 "SELECT type, id, {document} FROM definitions ORDER BY type, id"
1547 ))?;
1548 let mut rows = definitions.query([limit])?;
1549 while let Some(row) = rows.next()? {
1550 let (kind, id): (String, String) = (row.get(0)?, row.get(1)?);
1551 let stored = stored_document(
1552 utf8_column(row, 2, ErrorCode::Definition)?,
1553 "a definition document",
1554 ErrorCode::Definition,
1555 )?;
1556 if text(&stored, "format") != Some("tilewright.def/1")
1557 || text(&stored, "type") != Some(&kind)
1558 || text(&stored, "id") != Some(&id)
1559 {
1560 return Err(Error::Rejected(
1561 ErrorCode::Definition,
1562 format!("the document of definition {kind}/{id} disagrees with its row"),
1563 ));
1564 }
1565 }
1566 Ok(())
1567 }
1568
1569 fn check_stairwells(&self) -> Result<(), Error> {
1575 let mut stairwells = self.connection.prepare(&format!(
1576 "SELECT plane, id, {} FROM stairwells ORDER BY plane, id",
1577 bounded_text("document")
1578 ))?;
1579 let mut step_at = self.connection.prepare(
1580 "SELECT stairwell, step, proto FROM stairwell_steps WHERE plane = ?1 AND layer = ?2 AND x = ?3 AND y = ?4",
1581 )?;
1582 let mut listed: u64 = 0;
1583 let limits = Limits::default();
1587 let (mut current, mut count, mut steps) = (String::new(), 0_usize, 0_usize);
1588 let mut rows = stairwells.query([maximum_document_bytes()])?;
1589 while let Some(row) = rows.next()? {
1590 let (plane, id): (String, String) = (row.get(0)?, row.get(1)?);
1591 if plane != current {
1592 current.clone_from(&plane);
1593 (count, steps) = (0, 0);
1594 }
1595 count += 1;
1596 if count > limits.max_array_items {
1597 return Err(Error::Rejected(
1598 ErrorCode::Stairwell,
1599 format!("plane {plane} has more stairwells than a source document holds"),
1600 ));
1601 }
1602 let stored = stored_stairwell(&stored_document(
1603 utf8_column(row, 2, ErrorCode::Stairwell)?,
1604 "a stairwell document",
1605 ErrorCode::Stairwell,
1606 )?)?;
1607 steps = steps.saturating_add(stored.steps.len());
1608 if steps > limits.max_values {
1609 return Err(Error::Rejected(
1610 ErrorCode::Stairwell,
1611 format!("plane {plane} has more stairwell steps than a source document holds"),
1612 ));
1613 }
1614 let disagrees = || {
1615 Error::Rejected(
1616 ErrorCode::Stairwell,
1617 format!("the document of stairwell {plane}/{id} disagrees with its rows"),
1618 )
1619 };
1620 if stored.id != id {
1621 return Err(disagrees());
1622 }
1623 for (index, step) in stored.steps.iter().enumerate() {
1624 let found: Option<(String, i64, String)> = step_at
1625 .query_row(params![plane, step.layer, step.at[0], step.at[1]], |row| {
1626 Ok((row.get(0)?, row.get(1)?, row.get(2)?))
1627 })
1628 .optional()?;
1629 let expected = i64::try_from(index).map_err(|_| disagrees())?;
1630 if found.as_ref().is_none_or(|(stairwell, step_index, proto)| {
1631 *stairwell != id || *step_index != expected || *proto != step.proto
1632 }) {
1633 return Err(disagrees());
1634 }
1635 }
1636 listed = listed.saturating_add(u64::try_from(stored.steps.len()).unwrap_or(u64::MAX));
1637 }
1638 let rows: i64 =
1639 self.connection
1640 .query_row("SELECT count(*) FROM stairwell_steps", [], |row| row.get(0))?;
1641 if u64::try_from(rows).ok() != Some(listed) {
1642 return Err(Error::Rejected(
1643 ErrorCode::StairwellStep,
1644 "stairwell_steps holds a step that no stairwell lists".into(),
1645 ));
1646 }
1647 Ok(())
1648 }
1649
1650 fn profile_layers(&self) -> Result<(usize, usize, Option<u32>), Error> {
1653 let profile = stored_document(
1654 self.meta("profile")?,
1655 "the stored profile",
1656 ErrorCode::Profile,
1657 )?;
1658 let identity_ok = text(&profile, "id").is_some_and(is_identifier)
1661 && text(&profile, "version").is_some_and(|version| version.chars().count() <= 64);
1662 if text(&profile, "format") != Some("tilewright.profile/1")
1663 || !identity_ok
1664 || text(&profile, "id") != self.meta("profile_id")?.as_deref()
1665 || text(&profile, "version") != self.meta("profile_version")?.as_deref()
1666 {
1667 return Err(Error::Rejected(
1668 ErrorCode::Profile,
1669 "the stored profile disagrees with profile_id or profile_version".into(),
1670 ));
1671 }
1672 if !projection_ok(&profile) {
1673 return Err(Error::Rejected(
1674 ErrorCode::Profile,
1675 "the stored profile's projection is not one a source profile can have".into(),
1676 ));
1677 }
1678 let layer_height = stored_layer_height(&profile)?;
1679 let layers = |key: &str, range: std::ops::RangeInclusive<usize>| {
1682 let names: Option<Vec<&str>> = profile
1683 .root()
1684 .get(key)
1685 .and_then(Value::as_array)
1686 .filter(|items| range.contains(&items.len()))
1687 .and_then(|items| items.iter().map(Value::as_str).collect());
1688 names
1689 .filter(|names| {
1690 let unique: BTreeSet<&str> = names.iter().copied().collect();
1691 unique.len() == names.len() && names.iter().all(|name| is_identifier(name))
1692 })
1693 .ok_or_else(|| {
1694 Error::Rejected(
1695 ErrorCode::Profile,
1696 format!(
1697 "stored profile does not list {}..={} unique {key}",
1698 range.start(),
1699 range.end()
1700 ),
1701 )
1702 })
1703 };
1704 let render = layers("render_layers", 1..=usize::from(u16::MAX - 1))?;
1705 let ground = layers("ground_layers", 0..=MAXIMUM_GROUND_LAYERS)?;
1706 let known: BTreeSet<&str> = render.iter().copied().collect();
1707 if ground
1708 .iter()
1709 .any(|name| !known.contains(name) || ["height", "zone"].contains(name))
1710 {
1711 return Err(Error::Rejected(
1712 ErrorCode::Profile,
1713 "a stored ground layer is not a render layer other than height or zone".into(),
1714 ));
1715 }
1716 Ok((render.len(), ground.len(), layer_height))
1717 }
1718}
1719
1720struct Filled<R> {
1726 inner: R,
1727 missing: bool,
1728 failure: Option<std::io::Error>,
1729 head: Vec<u8>,
1731 hasher: Sha256,
1734}
1735
1736impl<R: std::io::Read> std::io::Read for Filled<R> {
1737 fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {
1738 if buffer.is_empty() {
1739 return Ok(0);
1740 }
1741 while !self.missing && self.failure.is_none() {
1742 match self.inner.read(buffer) {
1743 Ok(0) => self.missing = true,
1744 Ok(read) => {
1745 let wanted = HEADER_BYTES.saturating_sub(self.head.len()).min(read);
1746 self.head.extend_from_slice(&buffer[..wanted]);
1747 self.hasher.update(&buffer[..read]);
1748 return Ok(read);
1749 }
1750 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
1751 Err(error) => self.failure = Some(error),
1752 }
1753 }
1754 buffer.fill(0);
1755 Ok(buffer.len())
1756 }
1757}
1758
1759const HEADER_BYTES: usize = 20;
1761
1762fn check_header(head: &[u8]) -> Result<(), Error> {
1770 if head.len() == HEADER_BYTES
1771 && head.starts_with(b"SQLite format 3\0")
1772 && (head[18] != 1 || head[19] != 1)
1773 {
1774 return Err(Error::Rejected(
1775 ErrorCode::Identity,
1776 "the SQLite header's file format versions are not 1, a rollback journal's".into(),
1777 ));
1778 }
1779 Ok(())
1780}
1781
1782fn read_some(read: &mut impl std::io::Read) -> Result<bool, Error> {
1784 loop {
1785 match read.read(&mut [0]) {
1786 Ok(read) => return Ok(read != 0),
1787 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
1788 Err(error) => return Err(Error::Io(error)),
1789 }
1790 }
1791}
1792
1793fn harden(connection: &Connection) -> Result<(), Error> {
1796 connection.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?;
1797 connection.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?;
1798 connection.execute_batch("PRAGMA query_only = ON")?;
1799 Ok(())
1800}
1801
1802fn check_pages(connection: &Connection, length: u64, limits: &VerifyLimits) -> Result<(), Error> {
1806 let pages: i64 = connection.query_row("PRAGMA page_count", [], |row| row.get(0))?;
1807 if u64::try_from(pages)
1808 .ok()
1809 .and_then(|pages| pages.checked_mul(PAGE_SIZE.unsigned_abs()))
1810 != Some(length)
1811 {
1812 return Err(Error::Rejected(
1813 ErrorCode::FileLength,
1814 format!("the file is {length} bytes, not its {pages} pages of {PAGE_SIZE} bytes"),
1815 ));
1816 }
1817 if length > limits.maximum_pack_bytes {
1818 return Err(too_long(limits.maximum_pack_bytes));
1819 }
1820 Ok(())
1821}
1822
1823fn check_identity(connection: &Connection) -> Result<(), Error> {
1826 let mut schema = connection.prepare(&format!(
1828 "SELECT {}, {}, {} FROM sqlite_schema WHERE sql IS NOT NULL ORDER BY name LIMIT {}",
1829 bounded_text("type"),
1830 bounded_text("name"),
1831 bounded_text("sql"),
1832 SCHEMA_TABLES.len() + 1
1833 ))?;
1834 let mut found: Vec<(Option<String>, Option<String>, Option<String>)> = Vec::new();
1835 let mut rows = schema.query([bound(MAXIMUM_SCHEMA_BYTES)])?;
1836 while let Some(row) = rows.next()? {
1837 found.push((
1838 utf8_column(row, 0, ErrorCode::Schema)?,
1839 utf8_column(row, 1, ErrorCode::Schema)?,
1840 utf8_column(row, 2, ErrorCode::Schema)?,
1841 ));
1842 }
1843 drop(rows);
1844 drop(schema);
1845 let mut expected: Vec<(Option<String>, Option<String>, Option<String>)> = SCHEMA_TABLES
1846 .iter()
1847 .map(|(name, sql)| {
1848 (
1849 Some("table".to_owned()),
1850 Some((*name).to_owned()),
1851 Some((*sql).to_owned()),
1852 )
1853 })
1854 .collect();
1855 expected.sort_by(|left, right| left.1.cmp(&right.1));
1856 if found != expected {
1857 return Err(Error::Rejected(
1858 ErrorCode::Schema,
1859 "the pack schema does not match this format".into(),
1860 ));
1861 }
1862 let application: i32 = connection.query_row("PRAGMA application_id", [], |row| row.get(0))?;
1863 let version: i32 = connection.query_row("PRAGMA user_version", [], |row| row.get(0))?;
1864 let page_size: i64 = connection.query_row("PRAGMA page_size", [], |row| row.get(0))?;
1865 if application != APPLICATION_ID || version != USER_VERSION || page_size != PAGE_SIZE {
1866 return Err(Error::Rejected(
1867 ErrorCode::Identity,
1868 format!(
1869 "not a Tilewright pack of user_version {USER_VERSION} with {PAGE_SIZE}-byte pages"
1870 ),
1871 ));
1872 }
1873 let free: i64 = connection.query_row("PRAGMA freelist_count", [], |row| row.get(0))?;
1878 if free != 0 {
1879 return Err(Error::Rejected(
1880 ErrorCode::FreePages,
1881 format!("the pack has {free} free pages; a pack has none"),
1882 ));
1883 }
1884 let meta = read_meta(connection)?;
1885 let value = |key: &str| {
1886 meta.iter()
1887 .find(|(name, _)| name == key)
1888 .map(|(_, value)| value.as_str())
1889 };
1890 let digest_ok = value("source_digest").is_some_and(|digest| {
1891 digest.len() == 64
1892 && digest
1893 .bytes()
1894 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1895 });
1896 if value("format") != Some(FORMAT)
1897 || value("chunk_encoding") != Some(CHUNK_ENCODING)
1898 || !digest_ok
1899 {
1900 return Err(Error::Rejected(
1901 ErrorCode::Meta,
1902 format!("the pack's metadata is not that of {FORMAT}"),
1903 ));
1904 }
1905 Ok(())
1906}
1907
1908fn read_meta(connection: &Connection) -> Result<Vec<(String, String)>, Error> {
1911 let keys = META_KEYS
1912 .iter()
1913 .map(|key| format!("'{key}'"))
1914 .collect::<Vec<_>>()
1915 .join(", ");
1916 let (total, known, oversized): (i64, i64, i64) = connection.query_row(
1917 &format!(
1918 "SELECT count(*), count(*) FILTER (WHERE key IN ({keys})), \
1919 count(*) FILTER (WHERE typeof(value) != 'text' OR octet_length(value) > CASE key WHEN 'profile' THEN ?1 ELSE ?2 END) \
1920 FROM meta"
1921 ),
1922 params![maximum_document_bytes(), bound(MAXIMUM_META_BYTES)],
1923 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1924 )?;
1925 let expected = i64::try_from(META_KEYS.len()).unwrap_or(i64::MAX);
1926 if total != expected || known != expected || oversized != 0 {
1927 return Err(Error::Rejected(
1928 ErrorCode::Meta,
1929 format!("the pack's metadata is not that of {FORMAT}"),
1930 ));
1931 }
1932 let mut statement = connection.prepare("SELECT key, value FROM meta ORDER BY key")?;
1933 let mut rows = statement.query([])?;
1934 let mut meta = Vec::with_capacity(META_KEYS.len());
1935 while let Some(row) = rows.next()? {
1936 let value = utf8_column(row, 1, ErrorCode::Meta)?.unwrap_or_default();
1938 meta.push((row.get(0)?, value));
1939 }
1940 Ok(meta)
1941}
1942
1943struct Targets<'c> {
1949 strings: u32,
1950 render_layers: usize,
1951 is_plane: Statement<'c>,
1953 is_proto: Statement<'c>,
1955 is_zone: Statement<'c>,
1957 has_anchor: Statement<'c>,
1960 value: Statement<'c>,
1962 planes: BTreeMap<u32, bool>,
1963 protos: BTreeMap<u32, bool>,
1964 field_names: BTreeMap<u32, bool>,
1965 anchors: BTreeMap<(u32, u32), bool>,
1967 current: String,
1970 zones: BTreeMap<u32, bool>,
1971 own_anchors: BTreeMap<u32, bool>,
1972 uid_row: Statement<'c>,
1974 chunk: (i32, i64, i64),
1978 index: i64,
1979 uids: u64,
1980}
1981
1982impl<'c> Targets<'c> {
1983 fn new(connection: &'c Connection, strings: u32, render_layers: usize) -> Result<Self, Error> {
1986 Ok(Self {
1987 strings,
1988 render_layers,
1989 is_plane: connection.prepare(
1990 "SELECT EXISTS (SELECT 1 FROM strings s JOIN planes p ON p.id = s.value WHERE s.id = ?1)",
1991 )?,
1992 is_proto: connection.prepare(
1993 "SELECT EXISTS (SELECT 1 FROM strings s JOIN definitions d ON d.type = 'proto' AND d.id = s.value WHERE s.id = ?1)",
1994 )?,
1995 is_zone: connection.prepare(
1996 "SELECT EXISTS (SELECT 1 FROM strings s JOIN zones z ON z.plane = ?2 AND z.id = s.value WHERE s.id = ?1)",
1997 )?,
1998 has_anchor: connection.prepare(
1999 "SELECT EXISTS (SELECT 1 FROM strings n JOIN anchors a ON a.id = n.value \
2000 WHERE n.id = ?3 AND a.plane = CASE WHEN ?1 = 0 THEN ?2 ELSE (SELECT value FROM strings WHERE id = ?1) END)",
2001 )?,
2002 value: connection.prepare("SELECT value FROM strings WHERE id = ?1")?,
2003 uid_row: connection.prepare(
2004 "SELECT plane, layer, cx, cy, object FROM object_uids WHERE uid = ?1",
2005 )?,
2006 chunk: (0, 0, 0),
2007 index: 0,
2008 uids: 0,
2009 planes: BTreeMap::new(),
2010 protos: BTreeMap::new(),
2011 field_names: BTreeMap::new(),
2012 anchors: BTreeMap::new(),
2013 current: String::new(),
2014 zones: BTreeMap::new(),
2015 own_anchors: BTreeMap::new(),
2016 })
2017 }
2018}
2019
2020const CACHE_ENTRIES: usize = 1 << 16;
2024
2025fn cached<K: Ord + Copy>(
2027 cache: &mut BTreeMap<K, bool>,
2028 key: K,
2029 lookup: impl FnOnce() -> Result<bool, Error>,
2030) -> Result<bool, Error> {
2031 if let Some(known) = cache.get(&key) {
2032 return Ok(*known);
2033 }
2034 let found = lookup()?;
2035 if cache.len() >= CACHE_ENTRIES {
2036 cache.clear();
2037 }
2038 cache.insert(key, found);
2039 Ok(found)
2040}
2041
2042impl Targets<'_> {
2043 fn string(&self, id: u32) -> bool {
2044 (1..=self.strings).contains(&id)
2045 }
2046
2047 fn enter(&mut self, plane: &str, chunk: (i32, i64, i64)) {
2050 if self.current != plane {
2051 plane.clone_into(&mut self.current);
2052 self.zones.clear();
2053 self.own_anchors.clear();
2054 }
2055 self.chunk = chunk;
2056 self.index = 0;
2057 }
2058
2059 fn uid(&mut self, object: &ObjectRecord) -> Result<(), Error> {
2062 let index = self.index;
2063 self.index += 1;
2064 let Some(uid) = &object.uid else {
2065 return Ok(());
2066 };
2067 self.uids += 1;
2068 let row: Option<(
2069 types::Value,
2070 types::Value,
2071 types::Value,
2072 types::Value,
2073 types::Value,
2074 )> = self
2075 .uid_row
2076 .query_row([uid], |row| {
2077 Ok((
2078 row.get(0)?,
2079 row.get(1)?,
2080 row.get(2)?,
2081 row.get(3)?,
2082 row.get(4)?,
2083 ))
2084 })
2085 .optional()?;
2086 let (layer, cx, cy) = self.chunk;
2087 let expected = (
2088 types::Value::Text(self.current.clone()),
2089 types::Value::Integer(i64::from(layer)),
2090 types::Value::Integer(cx),
2091 types::Value::Integer(cy),
2092 types::Value::Integer(index),
2093 );
2094 if row.as_ref() == Some(&expected) {
2095 Ok(())
2096 } else {
2097 Err(Error::Rejected(
2098 ErrorCode::ObjectUid,
2099 format!("the object_uids row of {uid} does not name its object"),
2100 ))
2101 }
2102 }
2103
2104 fn proto(&mut self, id: u32) -> Result<bool, Error> {
2105 if !self.string(id) {
2106 return Ok(false);
2107 }
2108 let statement = &mut self.is_proto;
2109 cached(&mut self.protos, id, || {
2110 Ok(statement.query_row([id], |row| row.get(0))?)
2111 })
2112 }
2113
2114 fn field_name(&mut self, id: u32) -> Result<bool, Error> {
2115 if !self.string(id) {
2116 return Ok(false);
2117 }
2118 let statement = &mut self.value;
2119 cached(&mut self.field_names, id, || {
2120 let name: String = statement.query_row([id], |row| row.get(0))?;
2121 Ok(is_field_name(&name))
2122 })
2123 }
2124
2125 fn check_palette(&mut self, chunk: &ChunkData) -> Result<(), Error> {
2129 fn mark(cells: &[u16], used: &mut [bool]) {
2130 for cell in cells {
2131 if let Some(flag) = usize::from(*cell)
2132 .checked_sub(1)
2133 .and_then(|index| used.get_mut(index))
2134 {
2135 *flag = true;
2136 }
2137 }
2138 }
2139 let mut ground = vec![false; chunk.palette.len()];
2140 let mut zone = vec![false; chunk.palette.len()];
2141 for grid in &chunk.ground {
2142 mark(grid, &mut ground);
2143 }
2144 mark(&chunk.zones, &mut zone);
2145 for ((id, ground), zone) in chunk.palette.iter().zip(ground).zip(zone) {
2146 let id = *id;
2147 let mut ok = (ground || zone) && self.string(id) && (!ground || self.proto(id)?);
2148 if ok && zone {
2149 let (statement, plane) = (&mut self.is_zone, &self.current);
2150 ok = cached(&mut self.zones, id, || {
2151 Ok(statement.query_row(params![id, plane], |row| row.get(0))?)
2152 })?;
2153 }
2154 if !ok {
2155 return Err(Error::Rejected(
2156 ErrorCode::Reference,
2157 "a palette entry is unused or not a proto definition or zone of its plane"
2158 .into(),
2159 ));
2160 }
2161 }
2162 Ok(())
2163 }
2164
2165 fn portal(&mut self, portal: Option<&PortalRecord>) -> Result<bool, Error> {
2166 Ok(match portal {
2167 Some(PortalRecord::Location { x, y, .. })
2168 if [x, y]
2169 .iter()
2170 .any(|value| !(-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(*value)) =>
2171 {
2172 false
2173 }
2174 Some(PortalRecord::Location { plane: 0, .. }) | None => true,
2175 Some(PortalRecord::Location { plane, .. }) => {
2176 let plane = *plane;
2177 if !self.string(plane) {
2178 return Ok(false);
2179 }
2180 let statement = &mut self.is_plane;
2181 cached(&mut self.planes, plane, || {
2182 Ok(statement.query_row([plane], |row| row.get(0))?)
2183 })?
2184 }
2185 Some(PortalRecord::Anchor { plane, anchor }) => {
2186 let (plane, anchor) = (*plane, *anchor);
2187 if !(plane == 0 || self.string(plane)) || !self.string(anchor) {
2188 return Ok(false);
2189 }
2190 let (statement, current) = (&mut self.has_anchor, &self.current);
2191 let lookup =
2192 || Ok(statement.query_row(params![plane, current, anchor], |row| row.get(0))?);
2193 if plane == 0 {
2194 cached(&mut self.own_anchors, anchor, lookup)?
2195 } else {
2196 cached(&mut self.anchors, (plane, anchor), lookup)?
2197 }
2198 }
2199 })
2200 }
2201
2202 fn check(&mut self, object: &ObjectRecord) -> Result<(), Error> {
2203 self.uid(object)?;
2204 let mut ok = self.proto(object.proto)?
2205 && usize::from(object.layer) <= self.render_layers
2206 && self.portal(object.portal.as_ref())?;
2207 let mut names = BTreeSet::new();
2209 for (name, value) in &object.fields {
2210 ok &= names.insert(*name) && self.field_name(*name)?;
2211 match value {
2212 FieldRecord::String(id) => ok &= self.string(*id),
2213 FieldRecord::Integer(number) => {
2214 ok &= (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(number);
2215 }
2216 _ => {}
2217 }
2218 }
2219 if ok {
2220 Ok(())
2221 } else {
2222 Err(Error::Rejected(
2223 ErrorCode::Reference,
2224 "an object references a missing string, layer, plane or anchor, repeats or misnames a field, or has a portal position or integer field out of range".into(),
2225 ))
2226 }
2227 }
2228}
2229
2230#[cfg(test)]
2231mod tests {
2232 use super::{CACHE_ENTRIES, Pack, cached};
2233 use sha2::Digest as _;
2234 use std::{collections::BTreeMap, path::Path};
2235
2236 #[test]
2237 fn lookup_caches_stay_bounded() {
2238 let mut cache = BTreeMap::new();
2239 for key in 0..u32::try_from(CACHE_ENTRIES * 3).unwrap() {
2240 assert!(cached(&mut cache, key, || Ok(true)).unwrap());
2241 assert!(cache.len() <= CACHE_ENTRIES);
2242 }
2243 let last = u32::try_from(CACHE_ENTRIES * 3 - 1).unwrap();
2245 let mut again = false;
2246 let kept = cached(&mut cache, last, || {
2247 again = true;
2248 Ok(false)
2249 });
2250 assert!(kept.unwrap() && !again);
2251 let mut queried = false;
2252 let evicted = cached(&mut cache, 0, || {
2253 queried = true;
2254 Ok(false)
2255 });
2256 assert!(!evicted.unwrap() && queried);
2257 }
2258
2259 #[test]
2260 fn an_image_digest_is_of_sqlite_copy() {
2261 let path = crate::tests::valid_pack();
2262 let bytes = std::fs::read(path).unwrap();
2263 let mut pack = Pack::open_bytes(&bytes, None, &super::VerifyLimits::default()).unwrap();
2264 let digest = pack.file_digest().unwrap();
2265 let image = pack.connection.serialize(rusqlite::MAIN_DB).unwrap();
2268 assert!(matches!(image, rusqlite::serialize::Data::Shared(_)));
2269 assert_eq!(&*image, bytes.as_slice());
2270 assert_eq!(<[u8; 32]>::from(super::Sha256::digest(&*image)), digest);
2271 }
2272
2273 #[test]
2274 fn images_are_read_only() {
2275 let path = crate::tests::valid_pack();
2276 let bytes = std::fs::read(path).unwrap();
2277 let mut pack = Pack::open_bytes(&bytes, None, &super::VerifyLimits::default()).unwrap();
2278 pack.connection
2280 .execute_batch("PRAGMA query_only = OFF")
2281 .unwrap();
2282 for write in [
2283 "DELETE FROM strings",
2284 "CREATE TABLE extra (value)",
2285 "PRAGMA user_version = 3",
2286 ] {
2287 assert!(pack.connection.execute_batch(write).is_err(), "{write}");
2288 }
2289 pack.verify().unwrap();
2290 let image = pack.connection.serialize(rusqlite::MAIN_DB).unwrap();
2293 assert_eq!(&*image, bytes.as_slice());
2294 }
2295
2296 fn count(objects: &[crate::ObjectRecord]) -> u64 {
2298 objects
2299 .iter()
2300 .map(|object| {
2301 1 + u64::try_from(object.fields.len()).unwrap() + count(&object.inventory)
2302 })
2303 .sum()
2304 }
2305
2306 fn count_objects(objects: &[crate::ObjectRecord]) -> u64 {
2308 objects
2309 .iter()
2310 .map(|object| 1 + count_objects(&object.inventory))
2311 .sum()
2312 }
2313
2314 #[test]
2315 fn chunk_reads_keep_to_their_entry_budget() {
2316 let path = crate::tests::valid_pack();
2317 let pack = Pack::open(&path).unwrap();
2318 let chunk = pack.chunk("overworld", 0, 0, 0).unwrap().unwrap();
2319 let entries = count(&chunk.objects);
2320 assert!(
2321 entries > count_objects(&chunk.objects),
2322 "the chunk has fields"
2323 );
2324 assert_eq!(
2325 pack.chunk_within("overworld", 0, 0, 0, entries).unwrap(),
2326 Some(chunk)
2327 );
2328 let error = pack
2330 .chunk_within("overworld", 0, 0, 0, entries - 1)
2331 .unwrap_err();
2332 assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2333 assert!(
2336 pack.chunk_within("overworld", 1, 0, 0, 0)
2337 .unwrap()
2338 .is_some()
2339 );
2340 assert!(
2341 pack.chunk_within("overworld", 0, 9, 9, 0)
2342 .unwrap()
2343 .is_none()
2344 );
2345 let uid = "o.k3vq7zr2mxa5d6wfh4pnyt2cgu";
2347 assert!(pack.object_within(uid, entries).unwrap().is_some());
2348 let error = pack.object_within(uid, entries - 1).unwrap_err();
2349 assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2350 }
2351
2352 #[test]
2353 fn stairwell_reads_keep_to_their_step_budget() {
2354 let path = crate::tests::valid_pack();
2355 let pack = Pack::open(&path).unwrap();
2356 let mut stairwells = Vec::new();
2357 pack.visit_stairwells("overworld", crate::Page::all(), &mut |stairwell| {
2358 stairwells.push(stairwell);
2359 Ok(())
2360 })
2361 .unwrap();
2362 let longest = stairwells
2363 .iter()
2364 .max_by_key(|stairwell| stairwell.steps.len())
2365 .unwrap();
2366 let steps = u64::try_from(longest.steps.len()).unwrap();
2367 assert_eq!(
2368 pack.stairwell_within("overworld", &longest.id, steps)
2369 .unwrap()
2370 .as_ref(),
2371 Some(longest)
2372 );
2373 let error = pack
2374 .stairwell_within("overworld", &longest.id, steps - 1)
2375 .unwrap_err();
2376 assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2377 let mut listed = 0;
2378 pack.visit_stairwells_within("overworld", crate::Page::all(), steps, &mut |_| {
2379 listed += 1;
2380 Ok(())
2381 })
2382 .unwrap();
2383 assert_eq!(listed, stairwells.len());
2384 let error = pack
2385 .visit_stairwells_within("overworld", crate::Page::all(), steps - 1, &mut |_| Ok(()))
2386 .unwrap_err();
2387 assert_eq!(error.code(), crate::ErrorCode::DecodeBudget);
2388 }
2389
2390 #[test]
2391 fn packs_keep_the_absolute_path_they_opened() {
2392 let target = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target");
2394 std::fs::create_dir_all(&target).unwrap();
2395 let directory = tempfile::tempdir_in(&target).unwrap();
2396 let world = tilewright_source::load(
2397 &Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/basic"),
2398 )
2399 .unwrap();
2400 let written = crate::write_pack(&world, directory.path()).unwrap();
2401 let relative = Path::new("../../target")
2402 .join(directory.path().file_name().unwrap())
2403 .join(written.path.file_name().unwrap());
2404 let pack = Pack::open(&relative).unwrap();
2405 let super::Source::File(path) = &pack.source else {
2406 panic!("a pack opened from a path has a file source");
2407 };
2408 assert!(path.is_absolute(), "{}", path.display());
2409 pack.verify().unwrap();
2410 }
2411}