1use super::{
9 bounded_identifier, bounded_text, maximum_document_bytes,
10 rows::{
11 anchor_columns, anchor_row, chunk_coordinate, definition_columns, definition_row,
12 disagrees, identifier, integer, layer, plane_columns, plane_row, zone_columns, zone_row,
13 },
14 stored_document, stored_stairwell, utf8_column,
15};
16use crate::{Error, ErrorCode, MAXIMUM_OBJECT_INDEX, Pack};
17use rusqlite::Row;
18use tilewright_jsonc::Value;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum Identity<'a> {
25 RenderLayer {
27 name: &'a str,
28 position: usize,
29 },
30 Plane {
31 id: &'a str,
32 document: &'a str,
33 },
34 Zone {
35 plane: &'a str,
36 id: &'a str,
37 document: &'a str,
38 },
39 Anchor {
40 plane: &'a str,
41 id: &'a str,
42 layer: i32,
43 at: [i64; 2],
44 },
45 Stairwell {
46 plane: &'a str,
47 id: &'a str,
48 document: &'a str,
49 },
50 Definition {
51 kind: &'a str,
52 id: &'a str,
53 document: &'a str,
54 },
55 Chunk {
57 plane: &'a str,
58 layer: i32,
59 cx: i64,
60 cy: i64,
61 },
62 Object {
66 uid: &'a str,
67 plane: &'a str,
68 layer: i32,
69 cx: i64,
70 cy: i64,
71 index: usize,
72 },
73}
74
75impl Pack {
76 fn each_row(
79 &self,
80 sql: &str,
81 each: &mut dyn FnMut(&Row<'_>) -> Result<(), Error>,
82 ) -> Result<(), Error> {
83 let mut statement = self.connection.prepare(sql)?;
84 let mut rows = if statement.parameter_count() == 0 {
85 statement.query([])?
86 } else {
87 statement.query([maximum_document_bytes()])?
88 };
89 while let Some(row) = rows.next()? {
90 each(row)?;
91 }
92 Ok(())
93 }
94
95 pub fn visit_identities(
108 &self,
109 visit: &mut dyn FnMut(Identity<'_>) -> Result<(), Error>,
110 ) -> Result<(), Error> {
111 self.verify()?;
112 self.visit_rows(visit)
113 }
114
115 fn visit_rows(
130 &self,
131 visit: &mut dyn FnMut(Identity<'_>) -> Result<(), Error>,
132 ) -> Result<(), Error> {
133 let document = bounded_text("document");
134 let key = bounded_identifier;
135
136 self.visit_render_layers(visit)?;
137
138 self.each_row(
139 &format!("SELECT {} FROM planes ORDER BY id", plane_columns()),
140 &mut |row| {
141 let (id, stored) = plane_row(row)?;
142 visit(Identity::Plane {
143 id: &id,
144 document: stored.source(),
145 })
146 },
147 )?;
148 self.each_row(
149 &format!("SELECT {} FROM zones ORDER BY plane, id", zone_columns()),
150 &mut |row| {
151 let (plane, id, stored) = zone_row(row)?;
152 visit(Identity::Zone {
153 plane: &plane,
154 id: &id,
155 document: stored.source(),
156 })
157 },
158 )?;
159 self.each_row(
160 &format!(
161 "SELECT {}, {}, {document} FROM stairwells ORDER BY plane, id",
162 key("plane"),
163 key("id")
164 ),
165 &mut |row| {
166 let (plane, id) = (
167 identifier(row, 0, "stairwell", ErrorCode::Stairwell)?,
168 identifier(row, 1, "stairwell", ErrorCode::Stairwell)?,
169 );
170 let stairwell = stored_stairwell(&stored_document(
171 utf8_column(row, 2, ErrorCode::Stairwell)?,
172 "a stairwell document",
173 ErrorCode::Stairwell,
174 )?)?;
175 if stairwell.id != id {
176 return Err(disagrees(
177 &format!("stairwell {plane}/{id}"),
178 ErrorCode::Stairwell,
179 ));
180 }
181 visit(Identity::Stairwell {
182 plane: &plane,
183 id: &id,
184 document: &stairwell.canonical,
185 })
186 },
187 )?;
188 self.each_row(
189 &format!(
190 "SELECT {} FROM definitions ORDER BY type, id",
191 definition_columns()
192 ),
193 &mut |row| {
194 let (kind, id, stored) = definition_row(row)?;
195 visit(Identity::Definition {
196 kind: &kind,
197 id: &id,
198 document: stored.source(),
199 })
200 },
201 )?;
202 self.visit_positions(visit)
203 }
204
205 fn visit_render_layers(
207 &self,
208 visit: &mut dyn FnMut(Identity<'_>) -> Result<(), Error>,
209 ) -> Result<(), Error> {
210 self.profile_layers()?;
213 let profile = stored_document(
214 self.meta("profile")?,
215 "the stored profile",
216 ErrorCode::Profile,
217 )?;
218 let layers = profile
219 .root()
220 .get("render_layers")
221 .and_then(Value::as_array)
222 .ok_or_else(|| {
223 Error::Rejected(
224 ErrorCode::Profile,
225 "the stored profile lists no render layers".into(),
226 )
227 })?;
228 for (position, layer) in layers.iter().enumerate() {
229 let name = layer.as_str().ok_or_else(|| {
230 Error::Rejected(ErrorCode::Profile, "a render layer is not a string".into())
231 })?;
232 visit(Identity::RenderLayer { name, position })?;
233 }
234 Ok(())
235 }
236
237 fn visit_positions(
239 &self,
240 visit: &mut dyn FnMut(Identity<'_>) -> Result<(), Error>,
241 ) -> Result<(), Error> {
242 let key = bounded_identifier;
243 self.each_row(
244 &format!(
245 "SELECT {} FROM anchors ORDER BY plane, id",
246 anchor_columns()
247 ),
248 &mut |row| {
249 let anchor = anchor_row(row)?;
250 visit(Identity::Anchor {
251 plane: &anchor.plane,
252 id: &anchor.id,
253 layer: anchor.layer,
254 at: [anchor.x, anchor.y],
255 })
256 },
257 )?;
258 self.each_row(
259 &format!(
260 "SELECT {}, {}, {}, {} FROM chunks ORDER BY plane, layer, cx, cy",
261 key("plane"),
262 integer("layer"),
263 integer("cx"),
264 integer("cy")
265 ),
266 &mut |row| {
267 visit(Identity::Chunk {
268 plane: &identifier(row, 0, "chunk", ErrorCode::Chunk)?,
269 layer: layer(row, 1, "chunk", ErrorCode::Chunk)?,
270 cx: chunk_coordinate(row, 2, ErrorCode::Chunk)?,
271 cy: chunk_coordinate(row, 3, ErrorCode::Chunk)?,
272 })
273 },
274 )?;
275 self.each_row(
276 &format!(
277 "SELECT {}, {}, {}, {}, {}, {} FROM object_uids ORDER BY uid",
278 key("uid"),
279 key("plane"),
280 integer("layer"),
281 integer("cx"),
282 integer("cy"),
283 integer("object")
284 ),
285 &mut |row| {
286 let code = ErrorCode::ObjectUid;
287 let index: Option<i64> = row.get(5)?;
288 let index = index
289 .and_then(|index| usize::try_from(index).ok())
290 .filter(|index| *index <= MAXIMUM_OBJECT_INDEX)
291 .ok_or_else(|| {
292 Error::Rejected(
293 code,
294 format!(
295 "an object index is not an integer from 0 to {MAXIMUM_OBJECT_INDEX}"
296 ),
297 )
298 })?;
299 visit(Identity::Object {
300 uid: &identifier(row, 0, "uid", code)?,
301 plane: &identifier(row, 1, "uid", code)?,
302 layer: layer(row, 2, "uid", code)?,
303 cx: chunk_coordinate(row, 3, code)?,
304 cy: chunk_coordinate(row, 4, code)?,
305 index,
306 })
307 },
308 )
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::Identity;
315 use crate::{Error, Pack, write_pack};
316 use rusqlite::Connection;
317 use std::path::{Path, PathBuf};
318
319 fn world() -> tilewright_source::World {
320 let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../fixtures/basic");
321 tilewright_source::load(&fixture).expect("the fixture is valid")
322 }
323
324 fn pack(sql: &str) -> (tempfile::TempDir, PathBuf) {
326 let directory = tempfile::tempdir().unwrap();
327 let written = write_pack(&world(), directory.path()).unwrap();
328 let copy = directory.path().join("copy.twpack");
329 std::fs::copy(&written.path, ©).unwrap();
330 if !sql.is_empty() {
331 Connection::open(©)
332 .unwrap()
333 .execute_batch(&format!("{sql}; VACUUM;"))
334 .unwrap();
335 }
336 (directory, copy)
337 }
338
339 fn visit(path: &Path) -> Result<Vec<String>, Error> {
342 let mut listed = Vec::new();
343 Pack::open(path)?.visit_rows(&mut |identity| {
344 listed.push(match identity {
345 Identity::RenderLayer { name, position } => {
346 format!("render_layer {name} {position}")
347 }
348 Identity::Plane { id, document } => format!("plane {id} {document}"),
349 Identity::Zone {
350 plane,
351 id,
352 document,
353 } => format!("zone {plane}/{id} {document}"),
354 Identity::Anchor {
355 plane,
356 id,
357 layer,
358 at,
359 } => format!("anchor {plane}/{id} {layer} {at:?}"),
360 Identity::Stairwell {
361 plane,
362 id,
363 document,
364 } => format!("stairwell {plane}/{id} {document}"),
365 Identity::Definition { kind, id, document } => {
366 format!("definition {kind}/{id} {document}")
367 }
368 Identity::Chunk {
369 plane,
370 layer,
371 cx,
372 cy,
373 } => format!("chunk {plane} {layer} {cx} {cy}"),
374 Identity::Object {
375 uid,
376 plane,
377 layer,
378 cx,
379 cy,
380 index,
381 } => format!("object {uid} {plane} {layer} {cx} {cy} {index}"),
382 });
383 Ok(())
384 })?;
385 Ok(listed)
386 }
387
388 #[test]
389 fn identities_list_every_row_in_order_with_its_source_document() {
390 let world = world();
391 let mut expected = Vec::new();
392 for (position, name) in world.profile().render_layers.iter().enumerate() {
393 expected.push(format!("render_layer {name} {position}"));
394 }
395 for (id, plane) in world.planes() {
396 expected.push(format!("plane {id} {}", plane.canonical));
397 }
398 for (id, plane) in world.planes() {
399 for (zone, entry) in &plane.zones {
400 expected.push(format!("zone {id}/{zone} {}", entry.canonical));
401 }
402 }
403 for (id, plane) in world.planes() {
404 for (stairwell, entry) in &plane.stairwells {
405 expected.push(format!("stairwell {id}/{stairwell} {}", entry.canonical));
406 }
407 }
408 for ((kind, id), definition) in world.definitions() {
409 expected.push(format!("definition {kind}/{id} {}", definition.canonical));
410 }
411 for (id, plane) in world.planes() {
412 for (anchor, entry) in &plane.anchors {
413 expected.push(format!(
414 "anchor {id}/{anchor} {} {:?}",
415 entry.layer, entry.at
416 ));
417 }
418 }
419 for (id, plane) in world.planes() {
420 for (layer, cx, cy) in plane.chunks.keys() {
421 expected.push(format!("chunk {id} {layer} {cx} {cy}"));
422 }
423 }
424 let mut objects = Vec::new();
425 for (id, plane) in world.planes() {
426 for ((layer, cx, cy), chunk) in &plane.chunks {
427 for (index, object) in chunk.depth_first().enumerate() {
428 if let Some(uid) = &object.uid {
429 objects.push(format!("object {uid} {id} {layer} {cx} {cy} {index}"));
430 }
431 }
432 }
433 }
434 objects.sort();
435 assert_eq!(objects.len(), 5);
436 expected.extend(objects);
437 let (_directory, path) = pack("");
438 let listed = visit(&path).unwrap();
439 assert_eq!(listed, expected);
440 assert_eq!(
441 listed
442 .iter()
443 .filter(|line| line.starts_with("chunk "))
444 .count(),
445 5
446 );
447
448 let mut seen = 0;
450 let error = Pack::open(&path)
451 .unwrap()
452 .visit_identities(&mut |_| {
453 seen += 1;
454 Err(Error::Invalid("stop".into()))
455 })
456 .unwrap_err();
457 assert_eq!((seen, error.to_string()), (1, "stop".to_owned()));
458
459 let mut verified = 0;
461 Pack::open(&path)
462 .unwrap()
463 .visit_identities(&mut |_| {
464 verified += 1;
465 Ok(())
466 })
467 .unwrap();
468 assert_eq!(verified, expected.len());
469 }
470
471 #[test]
472 fn identities_are_listed_only_from_packs_that_verify() {
473 for sql in [
476 "UPDATE stairwell_steps SET proto = 'stairs_up' WHERE stairwell = 'cave.chute' AND step = 0",
477 "INSERT INTO stairwell_steps VALUES ('overworld', 5, 1, 1, 'cave.chute', 7, 'coin')",
478 "UPDATE planes SET document = replace(document, '\"wilderness\"', '\"nowhere\"') WHERE id = 'overworld'",
479 "UPDATE zones SET plane = 'ghost' WHERE id = 'village'",
480 "UPDATE zones SET parent = 'village', document = replace(document, '\"name\"', '\"parent\":\"village\",\"name\"') WHERE id = 'wilderness'",
481 "UPDATE anchors SET plane = 'ghost' WHERE id = 'village.well'",
482 "UPDATE chunks SET plane = 'ghost' WHERE plane = 'vault'",
483 "UPDATE stairwells SET plane = 'ghost' WHERE id = 'cave.chute'",
484 "UPDATE object_uids SET object = 5 WHERE uid = 'village.chest'",
485 "DELETE FROM object_uids WHERE uid = 'vault.chest'",
486 ] {
487 let (_directory, path) = pack(sql);
488 assert!(visit(&path).is_ok(), "{sql}: the row checks accept it");
489 let mut seen = 0;
490 let result = Pack::open(&path).unwrap().visit_identities(&mut |_| {
491 seen += 1;
492 Ok(())
493 });
494 assert!(result.is_err(), "{sql}");
495 assert_eq!(seen, 0, "{sql}");
496 }
497 }
498
499 const MALFORMED: &[(&str, &str)] = &[
501 (
502 "UPDATE planes SET document = replace(document, '\"vault\"', '\"other\"') WHERE id = 'vault'",
503 "disagrees",
504 ),
505 (
506 "UPDATE zones SET document = '{\"id\":\"elsewhere\"}' WHERE id = 'village'",
507 "disagrees",
508 ),
509 (
510 "UPDATE zones SET document = ' {}' WHERE id = 'village'",
511 "canonical",
512 ),
513 (
514 "UPDATE zones SET id = 'Village' WHERE id = 'village'",
515 "identifier",
516 ),
517 (
518 "UPDATE zones SET parent = 'wilderness' WHERE id = 'village'",
519 "disagrees",
520 ),
521 (
522 "UPDATE zones SET parent = NULL WHERE id = 'village'",
523 "disagrees",
524 ),
525 (
526 "UPDATE zones SET parent = 'x' || char(10) WHERE id = 'village'",
527 "identifier",
528 ),
529 (
530 "UPDATE planes SET kind = 'shared' WHERE id = 'vault'",
531 "disagrees",
532 ),
533 (
534 "UPDATE planes SET kind = 'odd', document = replace(document, '\"instanced\"', '\"odd\"') WHERE id = 'vault'",
535 "invalid kind",
536 ),
537 (
538 "UPDATE planes SET chunk_size = 16 WHERE id = 'vault'",
539 "chunk size",
540 ),
541 (
542 "UPDATE planes SET document = replace(document, 'plane/1', 'plane/2') WHERE id = 'vault'",
543 "disagrees",
544 ),
545 (
546 "UPDATE anchors SET layer = 4294967296 WHERE id = 'village.well'",
547 "32-bit",
548 ),
549 (
550 "UPDATE anchors SET x = 'one' WHERE id = 'village.well'",
551 "position",
552 ),
553 (
554 "UPDATE anchors SET y = 9007199254740992 WHERE id = 'village.well'",
555 "position",
556 ),
557 (
558 "UPDATE stairwells SET document = replace(document, '\"down\"', '\"sideways\"') WHERE id = 'cave.chute'",
559 "shape",
560 ),
561 (
562 "UPDATE stairwells SET id = 'cave.slide' WHERE id = 'cave.chute'",
563 "disagrees",
564 ),
565 (
566 "UPDATE definitions SET type = 'loot' WHERE type = 'recipe'",
567 "disagrees",
568 ),
569 (
570 "UPDATE definitions SET document = zeroblob(10) WHERE id = 'coin'",
571 "canonical",
572 ),
573 ("UPDATE chunks SET layer = 'x' WHERE layer = 1", "32-bit"),
574 (
575 "UPDATE meta SET value = '{\"render_layers\":[]}' WHERE key = 'profile'",
576 "profile",
577 ),
578 (
579 "UPDATE meta SET value = replace(value, '\"effect\"]', '\"effect\",\"floor\"]') WHERE key = 'profile'",
580 "render_layers",
581 ),
582 (
583 "UPDATE meta SET value = '2.0.0' WHERE key = 'profile_version'",
584 "profile_version",
585 ),
586 (
587 "UPDATE chunks SET cx = 281474976710656 WHERE plane = 'vault'",
588 "chunk coordinate",
589 ),
590 (
591 "UPDATE chunks SET cy = -281474976710656 WHERE plane = 'vault'",
592 "chunk coordinate",
593 ),
594 (
595 "UPDATE chunks SET plane = x'00' WHERE plane = 'vault'",
596 "identifier",
597 ),
598 (
599 "UPDATE object_uids SET uid = 'Vault.chest' WHERE uid = 'vault.chest'",
600 "uid key",
601 ),
602 (
603 "UPDATE object_uids SET plane = 'Vault' WHERE uid = 'vault.chest'",
604 "uid key",
605 ),
606 (
607 "UPDATE object_uids SET layer = 2147483648 WHERE uid = 'vault.chest'",
608 "32-bit",
609 ),
610 (
611 "UPDATE object_uids SET cx = 281474976710656 WHERE uid = 'vault.chest'",
612 "chunk coordinate",
613 ),
614 (
615 "UPDATE object_uids SET object = -1 WHERE uid = 'vault.chest'",
616 "object index",
617 ),
618 (
619 "UPDATE object_uids SET object = 16777216 WHERE uid = 'vault.chest'",
620 "object index",
621 ),
622 ];
623
624 #[test]
625 fn identities_reject_rows_of_the_wrong_shape() {
626 for (sql, problem) in MALFORMED {
627 let (_directory, path) = pack(sql);
628 let error = visit(&path).expect_err(sql).to_string();
629 assert!(error.contains(problem), "{sql}: {error}");
630 }
631 let (_directory, path) = pack(
633 "UPDATE chunks SET cx = 281474976710655, cy = -281474976710655 WHERE plane = 'vault'",
634 );
635 assert!(
636 visit(&path)
637 .unwrap()
638 .contains(&"chunk vault 0 281474976710655 -281474976710655".to_owned())
639 );
640 }
641}