Skip to main content

tilewright_pack/
compile.rs

1// Copyright 2026 Seraphina Studios
2// SPDX-License-Identifier: MIT
3
4//! Deterministic compilation of a validated world into a pack.
5
6use crate::{
7    APPLICATION_ID, Error, FORMAT, Pack, SCHEMA, USER_VERSION,
8    chunk::{
9        CHUNK_ENCODING, ChunkData, FieldRecord, MAXIMUM_DEPTH, MAXIMUM_RECORD_BYTES, ObjectRecord,
10        PortalRecord, encode_chunk,
11    },
12    is_field_name, is_identifier,
13};
14use rusqlite::{Connection, MAIN_DB, params};
15use sha2::{Digest, Sha256};
16use std::{
17    borrow::Cow,
18    collections::{BTreeMap, BTreeSet},
19    io::{Read as _, Write as _},
20    path::{Path, PathBuf},
21};
22use tilewright_jsonc::{Limits, MAX_SAFE_INTEGER};
23use tilewright_source::{
24    CHUNK_SIZE, Chunk, FieldValue, Grid, Object, Plane, Portal, Profile, World,
25};
26
27/// The `compiler` meta value.
28pub const COMPILER: &str = concat!("tilewright-pack ", env!("CARGO_PKG_VERSION"));
29
30/// Interned strings: IDs start at 1 in byte order of the values; 0 means
31/// "none" wherever an ID is optional.
32/// The strings are borrowed from the world, and each is checked against its
33/// role's limits before it is kept: the model is public.
34pub(crate) struct Strings<'w>(BTreeMap<&'w str, u32>);
35
36/// `value` if it is an identifier.
37pub(crate) fn identifier(value: &str) -> Result<&str, Error> {
38    if is_identifier(value) {
39        Ok(value)
40    } else {
41        Err(Error::Invalid(format!(
42            "{:?} is not an identifier",
43            value.chars().take(128).collect::<String>()
44        )))
45    }
46}
47
48/// `value` if it is a field name.
49pub(crate) fn field_name(value: &str) -> Result<&str, Error> {
50    if is_field_name(value) {
51        Ok(value)
52    } else {
53        Err(Error::Invalid("a field name is not valid".into()))
54    }
55}
56
57/// `value` if it fits a source string: at most 1 MiB, without NUL.
58pub(crate) fn text(value: &str) -> Result<&str, Error> {
59    if value.len() <= Limits::default().max_string_bytes && !value.contains('\0') {
60        Ok(value)
61    } else {
62        Err(Error::Invalid(
63            "a string value is longer than 1 MiB or contains NUL".into(),
64        ))
65    }
66}
67
68/// `value` if it fits a source document: at most 64 MiB.
69pub(crate) fn document(value: &str) -> Result<&str, Error> {
70    if value.len() <= Limits::default().max_input_bytes {
71        Ok(value)
72    } else {
73        Err(Error::Invalid(
74            "a stored document is longer than 64 MiB".into(),
75        ))
76    }
77}
78
79impl<'w> Strings<'w> {
80    pub(crate) fn collect(world: &'w World) -> Result<Self, Error> {
81        let mut values: BTreeSet<&'w str> = BTreeSet::new();
82        for (id, plane) in world.planes() {
83            if plane.chunk_size != CHUNK_SIZE {
84                return Err(Error::Invalid(format!(
85                    "plane {id} has chunk size {}, not {CHUNK_SIZE}",
86                    plane.chunk_size
87                )));
88            }
89            for chunk in plane.chunks.values() {
90                check_chunk_shape(chunk, world.profile().ground_layers.len())?;
91                for grid in chunk.ground.iter().chain([&chunk.zones]) {
92                    grid_values(&mut values, grid)?;
93                }
94                let mut stack: Vec<&Object> = chunk.objects.iter().collect();
95                while let Some(object) = stack.pop() {
96                    values.insert(identifier(&object.proto)?);
97                    for (name, value) in &object.fields {
98                        values.insert(field_name(name)?);
99                        if let FieldValue::String(value) = value {
100                            values.insert(text(value)?);
101                        }
102                    }
103                    match &object.portal {
104                        Some(Portal::Location {
105                            plane: Some(plane), ..
106                        }) => {
107                            values.insert(identifier(plane)?);
108                        }
109                        Some(Portal::Anchor { plane, anchor }) => {
110                            if let Some(plane) = plane {
111                                values.insert(identifier(plane)?);
112                            }
113                            values.insert(identifier(anchor)?);
114                        }
115                        Some(Portal::Location { plane: None, .. }) | None => {}
116                    }
117                    stack.extend(&object.inventory);
118                }
119            }
120        }
121        // Resolved ground grids hold the source's prototypes and those the
122        // terrain transitions draw, so every prototype the transitions name
123        // is interned, and chunks are resolved once, as they are encoded.
124        if let Some(transitions) = &world.profile().transitions {
125            for proto in transitions.protos() {
126                values.insert(identifier(proto)?);
127            }
128        }
129        let mut ids = BTreeMap::new();
130        for (index, value) in values.into_iter().enumerate() {
131            let id =
132                u32::try_from(index + 1).map_err(|_| Error::Invalid("too many strings".into()))?;
133            ids.insert(value, id);
134        }
135        Ok(Self(ids))
136    }
137
138    fn id(&self, value: &str) -> Result<u32, Error> {
139        self.0
140            .get(value)
141            .copied()
142            .ok_or_else(|| Error::Invalid(format!("string \"{value}\" was not interned")))
143    }
144
145    fn optional(&self, value: Option<&String>) -> Result<u32, Error> {
146        value.map_or(Ok(0), |value| self.id(value))
147    }
148}
149
150/// Adds the values a grid's cells use to `values`.
151fn grid_values<'w>(values: &mut BTreeSet<&'w str>, grid: &'w Grid) -> Result<(), Error> {
152    let used: BTreeSet<u16> = grid.cells.iter().copied().collect();
153    for cell in used {
154        if let Some(index) = usize::from(cell).checked_sub(1) {
155            values.insert(identifier(palette_value(grid, index)?)?);
156        }
157    }
158    Ok(())
159}
160
161/// The palette value of a grid cell's index.
162pub(crate) fn palette_value(grid: &tilewright_source::Grid, index: usize) -> Result<&str, Error> {
163    grid.palette
164        .get(index)
165        .map(String::as_str)
166        .ok_or_else(|| Error::Invalid("a grid cell has no palette entry".into()))
167}
168
169/// Render layer names to their record value, index plus one.
170pub(crate) type Layers<'p> = BTreeMap<&'p str, u16>;
171
172pub(crate) fn render_layers(profile: &Profile) -> Result<Layers<'_>, Error> {
173    profile
174        .render_layers
175        .iter()
176        .enumerate()
177        .map(|(index, name)| {
178            u16::try_from(index + 1)
179                .map(|value| (name.as_str(), value))
180                .map_err(|_| Error::Invalid("too many render layers".into()))
181        })
182        .collect()
183}
184
185/// Checks that the stored profile document lists the same render and ground
186/// layers, in the same order, and the same layer height as the typed
187/// profile the records are built from: the model is public, and readers
188/// interpret indexes and elevations by the document.
189pub(crate) fn check_profile_document(profile: &Profile) -> Result<(), Error> {
190    let document =
191        tilewright_jsonc::Document::parse(profile.canonical.as_str(), &crate::canonical_limits())
192            .map_err(|error| Error::Invalid(format!("the profile document: {error}")))?;
193    let names = |key: &str| -> Option<Vec<&str>> {
194        document
195            .root()
196            .get(key)?
197            .as_array()?
198            .iter()
199            .map(tilewright_jsonc::Value::as_str)
200            .collect()
201    };
202    let same = |key: &str, typed: &[String]| {
203        names(key).is_some_and(|names| names.iter().copied().eq(typed.iter().map(String::as_str)))
204    };
205    let layer_height = document
206        .root()
207        .get("layer_height")
208        .map(tilewright_jsonc::Value::as_i64);
209    if same("render_layers", &profile.render_layers)
210        && same("ground_layers", &profile.ground_layers)
211        && layer_height == profile.layer_height.map(|height| Some(i64::from(height)))
212    {
213        Ok(())
214    } else {
215        Err(Error::Invalid(
216            "the profile document's layers or layer height differ from the typed profile".into(),
217        ))
218    }
219}
220
221/// Checks that the profile's transitions resolve to one grid per ground
222/// layer: the model is public, so they may come from another profile.
223pub(crate) fn check_transitions(profile: &Profile) -> Result<(), Error> {
224    match &profile.transitions {
225        Some(transitions) if transitions.ground_layers() != profile.ground_layers.len() => {
226            Err(Error::Invalid(
227                "the profile's transitions were read for a profile with other ground layers".into(),
228            ))
229        }
230        _ => Ok(()),
231    }
232}
233
234/// Checks a public model's chunk against format 1 before anything is
235/// copied from it.
236pub(crate) fn check_chunk_shape(chunk: &Chunk, ground_layers: usize) -> Result<(), Error> {
237    let cells = usize::from(CHUNK_SIZE) * usize::from(CHUNK_SIZE);
238    let grid_ok = |grid: &tilewright_source::Grid| {
239        grid.cells.len() == cells && grid.palette.len() < usize::from(u16::MAX)
240    };
241    if chunk.ground.len() == ground_layers
242        && chunk.ground.iter().all(grid_ok)
243        && grid_ok(&chunk.zones)
244        && chunk.heights.len() == cells
245    {
246        Ok(())
247    } else {
248        Err(Error::Invalid(format!(
249            "chunk {} does not have the grids of a format-1 chunk",
250            chunk.path
251        )))
252    }
253}
254
255/// The bytes an object's own part of a record takes, inventory excluded.
256fn encoded_size(object: &Object) -> usize {
257    let uid = object
258        .uid
259        .as_ref()
260        .map_or(0, |uid| uid.len().saturating_add(1));
261    let portal: usize = match object.portal {
262        Some(Portal::Location { .. }) => 25,
263        Some(Portal::Anchor { .. }) => 9,
264        None => 0,
265    };
266    let fields = if object.fields.is_empty() {
267        0
268    } else {
269        object.fields.iter().fold(2_usize, |total, (_, value)| {
270            total.saturating_add(match value {
271                FieldValue::Null | FieldValue::Bool(_) => 5,
272                FieldValue::Integer(_) | FieldValue::Number(_) => 13,
273                FieldValue::String(_) => 9,
274                FieldValue::Json(text) => text.len().saturating_add(9),
275            })
276        })
277    };
278    let custom = object
279        .custom
280        .as_ref()
281        .map_or(0, |text| text.len().saturating_add(4));
282    let inventory = if object.inventory.is_empty() { 0 } else { 4 };
283    (15 + portal + inventory)
284        .saturating_add(uid)
285        .saturating_add(fields)
286        .saturating_add(custom)
287}
288
289/// Builds an object record. `depth` is the object's inventory depth and
290/// `budget` the bytes left in the chunk's record; the model is public, so
291/// its bounds are checked here rather than assumed.
292pub(crate) fn object_record(
293    object: &Object,
294    layers: &Layers<'_>,
295    strings: &Strings<'_>,
296    depth: usize,
297    budget: &mut usize,
298) -> Result<ObjectRecord, Error> {
299    if depth > MAXIMUM_DEPTH {
300        return Err(Error::Invalid(format!(
301            "inventories nest more than {MAXIMUM_DEPTH} deep"
302        )));
303    }
304    *budget = budget
305        .checked_sub(encoded_size(object))
306        .ok_or_else(|| Error::Invalid("a chunk's record exceeds 16 MiB".into()))?;
307    let chunk_size = CHUNK_SIZE;
308    let outside = || {
309        Error::Invalid(
310            "top-level objects need a tile in their chunk; inventory entries none".into(),
311        )
312    };
313    let tile = match (object.at, depth) {
314        (Some([x, y]), 0) if x < chunk_size && y < chunk_size => Some(
315            y.checked_mul(chunk_size)
316                .and_then(|row| row.checked_add(x))
317                .ok_or_else(outside)?,
318        ),
319        (None, 1..) => None,
320        _ => return Err(outside()),
321    };
322    let layer = match &object.layer {
323        Some(name) => layers
324            .get(name.as_str())
325            .copied()
326            .ok_or_else(|| Error::Invalid(format!("unknown render layer \"{name}\"")))?,
327        None => 0,
328    };
329    let portal = match &object.portal {
330        Some(Portal::Location { at, .. })
331            if at
332                .iter()
333                .any(|value| !(-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(value)) =>
334        {
335            return Err(Error::Invalid(
336                "a portal leads outside the format's integer range".into(),
337            ));
338        }
339        Some(Portal::Location { plane, layer, at }) => Some(PortalRecord::Location {
340            plane: strings.optional(plane.as_ref())?,
341            layer: *layer,
342            x: at[0],
343            y: at[1],
344        }),
345        Some(Portal::Anchor { plane, anchor }) => Some(PortalRecord::Anchor {
346            plane: strings.optional(plane.as_ref())?,
347            anchor: strings.id(anchor)?,
348        }),
349        None => None,
350    };
351    let mut fields = Vec::with_capacity(object.fields.len());
352    for (name, value) in &object.fields {
353        let record = match value {
354            FieldValue::Null => FieldRecord::Null,
355            FieldValue::Bool(flag) => FieldRecord::Bool(*flag),
356            FieldValue::Integer(number) => FieldRecord::Integer(*number),
357            FieldValue::Number(number) => FieldRecord::Number(*number),
358            FieldValue::String(text) => FieldRecord::String(strings.id(text)?),
359            FieldValue::Json(text) => FieldRecord::Json(text.clone()),
360        };
361        fields.push((strings.id(name)?, record));
362    }
363    let inventory = object
364        .inventory
365        .iter()
366        .map(|item| object_record(item, layers, strings, depth + 1, budget))
367        .collect::<Result<_, _>>()?;
368    let uid = match &object.uid {
369        Some(uid) => Some(identifier(uid)?.to_owned()),
370        None => None,
371    };
372    Ok(ObjectRecord {
373        uid,
374        tile,
375        proto: strings.id(&object.proto)?,
376        layer,
377        sub_layer: object.sub_layer,
378        z: object.z,
379        portal,
380        fields,
381        custom: object.custom.clone(),
382        inventory,
383    })
384}
385
386/// A chunk's ground grids as its record holds them: with the profile's
387/// terrain transitions applied (FORMAT.md section 13), or the source's own
388/// when the profile has none.
389fn record_ground<'w>(
390    world: &'w World,
391    plane: &'w Plane,
392    (layer, cx, cy): (i32, i64, i64),
393    chunk: &'w Chunk,
394) -> Cow<'w, [Grid]> {
395    match &world.profile().transitions {
396        Some(transitions) => Cow::Owned(transitions.resolve_chunk(plane, layer, cx, cy).grids()),
397        None => Cow::Borrowed(&chunk.ground),
398    }
399}
400
401/// Builds a chunk record. The chunk palette holds the string IDs its grids
402/// use, in ascending order.
403fn chunk_record(
404    chunk: &Chunk,
405    ground: &[Grid],
406    layers: &Layers<'_>,
407    strings: &Strings<'_>,
408) -> Result<ChunkData, Error> {
409    let chunk_size = CHUNK_SIZE;
410    let mut used = BTreeSet::new();
411    for grid in ground.iter().chain([&chunk.zones]) {
412        for cell in &grid.cells {
413            if let Some(index) = usize::from(*cell).checked_sub(1) {
414                used.insert(strings.id(palette_value(grid, index)?)?);
415            }
416        }
417    }
418    let palette: Vec<u32> = used.into_iter().collect();
419    // Objects may use what the header (14 bytes through the palette length),
420    // palette, grids and object count (4 bytes) leave of the record.
421    let cells = usize::from(chunk_size) * usize::from(chunk_size);
422    let mut budget = MAXIMUM_RECORD_BYTES
423        .saturating_sub(18 + 4 * palette.len() + cells * (2 * ground.len() + 6));
424    // Declared palette entries that no cell uses are not interned; they map
425    // to 0 and are never looked up.
426    let remap = |grid: &tilewright_source::Grid| -> Result<Vec<u16>, Error> {
427        let local: Vec<u16> = grid
428            .palette
429            .iter()
430            .map(|value| {
431                strings
432                    .0
433                    .get(value.as_str())
434                    .and_then(|id| palette.binary_search(id).ok())
435                    .and_then(|index| u16::try_from(index + 1).ok())
436                    .unwrap_or(0)
437            })
438            .collect();
439        grid.cells
440            .iter()
441            .map(|cell| match usize::from(*cell).checked_sub(1) {
442                None => Ok(0),
443                Some(index) => match local.get(index) {
444                    Some(0) | None => {
445                        Err(Error::Invalid("a grid cell has no palette entry".into()))
446                    }
447                    Some(mapped) => Ok(*mapped),
448                },
449            })
450            .collect()
451    };
452    Ok(ChunkData {
453        chunk_size,
454        ground: ground.iter().map(remap).collect::<Result<_, _>>()?,
455        heights: chunk.heights.clone(),
456        zones: remap(&chunk.zones)?,
457        objects: chunk
458            .objects
459            .iter()
460            .map(|object| object_record(object, layers, strings, 0, &mut budget))
461            .collect::<Result<_, _>>()?,
462        palette,
463    })
464}
465
466/// An `object_uids` row by uid, so rows are inserted in primary-key order:
467/// the plane, layer and chunk coordinates of the object's chunk, and its
468/// index in the chunk record's depth-first order.
469pub(crate) type UidRows<'w> = BTreeMap<&'w str, (&'w str, i32, i64, i64, i64)>;
470
471/// Adds the uids of a chunk's objects, at every depth, to `rows`. The chunk
472/// has been encoded, which bounds its inventories' depth. The model is
473/// public, so uniqueness is checked here rather than assumed.
474pub(crate) fn collect_uids<'w>(
475    rows: &mut UidRows<'w>,
476    plane: &'w str,
477    (layer, cx, cy): (i32, i64, i64),
478    chunk: &'w Chunk,
479) -> Result<(), Error> {
480    // Depth-first order is the order of the chunk's record.
481    for (index, object) in (0_i64..).zip(chunk.depth_first()) {
482        if let Some(uid) = &object.uid
483            && rows
484                .insert(identifier(uid)?, (plane, layer, cx, cy, index))
485                .is_some()
486        {
487            return Err(Error::Invalid(format!("two objects have uid {uid}")));
488        }
489    }
490    Ok(())
491}
492
493/// A stairwell step's row: its stairwell, its index in the stairwell, and
494/// its prototype.
495pub(crate) type TileStep<'w> = (&'w str, i64, &'w str);
496
497/// The steps of a plane's stairwells by tile, the `stairwell_steps` key, so
498/// they are inserted in primary-key order. The model is public, so the
499/// plane's stairwell and step counts, and each stairwell's ID and step
500/// prototypes, are checked against the source format's limits before
501/// anything is kept.
502pub(crate) fn stairwell_tiles(
503    plane: &tilewright_source::Plane,
504) -> Result<BTreeMap<(i32, i64, i64), TileStep<'_>>, Error> {
505    // One stairwells document holds at most the source format's array limit
506    // of stairwells and value limit of steps, as verification checks.
507    let limits = Limits::default();
508    if plane.stairwells.len() > limits.max_array_items {
509        return Err(Error::Invalid(format!(
510            "plane {} has more stairwells than a source document holds",
511            plane.id
512        )));
513    }
514    let steps = plane
515        .stairwells
516        .values()
517        .try_fold(0_usize, |total, stairwell| {
518            total.checked_add(stairwell.steps.len())
519        });
520    if steps.is_none_or(|steps| steps > limits.max_values) {
521        return Err(Error::Invalid(format!(
522            "plane {} has more stairwell steps than a source document holds",
523            plane.id
524        )));
525    }
526    let mut tiles = BTreeMap::new();
527    for (id, stairwell) in &plane.stairwells {
528        let id = identifier(id)?;
529        for (index, step) in stairwell.steps.iter().enumerate() {
530            let index = i64::try_from(index)
531                .map_err(|_| Error::Invalid("a stairwell has too many steps".into()))?;
532            let tile = (step.layer, step.at[0], step.at[1]);
533            if tiles
534                .insert(tile, (id, index, identifier(&step.proto)?))
535                .is_some()
536            {
537                return Err(Error::Invalid(format!(
538                    "two stairwell steps of plane {} share layer {} tile [{}, {}]",
539                    plane.id, tile.0, tile.1, tile.2
540                )));
541            }
542        }
543    }
544    Ok(tiles)
545}
546
547/// Inserts a plane's stairwells and their steps, each in primary-key order.
548fn insert_stairwells(
549    connection: &Connection,
550    id: &str,
551    plane: &tilewright_source::Plane,
552) -> Result<(), Error> {
553    // Checks every stairwell before any is inserted.
554    let tiles = stairwell_tiles(plane)?;
555    let mut stairwells =
556        connection.prepare("INSERT INTO stairwells (plane, id, document) VALUES (?1, ?2, ?3)")?;
557    for (stairwell_id, stairwell) in &plane.stairwells {
558        stairwells.execute(params![id, stairwell_id, document(&stairwell.canonical)?])?;
559    }
560    let mut steps = connection.prepare(
561        "INSERT INTO stairwell_steps (plane, layer, x, y, stairwell, step, proto) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
562    )?;
563    for ((layer, x, y), (stairwell, step, proto)) in tiles {
564        steps.execute(params![id, layer, x, y, stairwell, step, proto])?;
565    }
566    Ok(())
567}
568
569pub(crate) fn hex(bytes: &[u8]) -> String {
570    use std::fmt::Write as _;
571    bytes.iter().fold(String::new(), |mut output, byte| {
572        let _ = write!(output, "{byte:02x}");
573        output
574    })
575}
576
577/// Compiles a world into pack bytes. The same world and compiler version
578/// always produce the same bytes.
579///
580/// # Errors
581/// Returns an error if SQLite fails or the world exceeds the pack's bounds.
582pub fn compile(world: &World) -> Result<Vec<u8>, Error> {
583    check_transitions(world.profile())?;
584    let strings = Strings::collect(world)?;
585    check_profile_document(world.profile())?;
586    let layers = render_layers(world.profile())?;
587    let connection = Connection::open_in_memory()?;
588    let schema: &str = &SCHEMA;
589    connection.execute_batch(&format!(
590        "PRAGMA page_size = 4096; PRAGMA journal_mode = OFF; PRAGMA synchronous = OFF;
591         PRAGMA application_id = {APPLICATION_ID}; PRAGMA user_version = {USER_VERSION}; {schema}"
592    ))?;
593    connection.execute_batch("BEGIN")?;
594    {
595        let profile = world.profile();
596        let digest = hex(&world.source_digest());
597        let mut meta = connection.prepare("INSERT INTO meta (key, value) VALUES (?1, ?2)")?;
598        for (key, value) in [
599            ("chunk_encoding", CHUNK_ENCODING),
600            ("compiler", COMPILER),
601            ("format", FORMAT),
602            ("profile", document(&profile.canonical)?),
603            ("profile_id", profile.id.as_str()),
604            ("profile_version", profile.version.as_str()),
605            ("source_digest", digest.as_str()),
606        ] {
607            meta.execute(params![key, value])?;
608        }
609        let mut insert = connection.prepare("INSERT INTO strings (id, value) VALUES (?1, ?2)")?;
610        let mut by_id: Vec<(&u32, &&str)> =
611            strings.0.iter().map(|(value, id)| (id, value)).collect();
612        by_id.sort();
613        for (id, value) in by_id {
614            insert.execute(params![id, value])?;
615        }
616        let mut planes = connection.prepare(
617            "INSERT INTO planes (id, kind, chunk_size, document) VALUES (?1, ?2, ?3, ?4)",
618        )?;
619        let mut zones = connection
620            .prepare("INSERT INTO zones (plane, id, parent, document) VALUES (?1, ?2, ?3, ?4)")?;
621        let mut anchors = connection
622            .prepare("INSERT INTO anchors (plane, id, layer, x, y) VALUES (?1, ?2, ?3, ?4, ?5)")?;
623        let mut chunks = connection.prepare(
624            "INSERT INTO chunks (plane, layer, cx, cy, encoding, data) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
625        )?;
626        let mut uids = UidRows::new();
627        for (id, plane) in world.planes() {
628            planes.execute(params![
629                id,
630                plane.kind.as_str(),
631                plane.chunk_size,
632                document(&plane.canonical)?
633            ])?;
634            for (zone_id, zone) in &plane.zones {
635                zones.execute(params![
636                    id,
637                    zone_id,
638                    zone.parent,
639                    document(&zone.canonical)?
640                ])?;
641            }
642            for (anchor_id, anchor) in &plane.anchors {
643                anchors.execute(params![
644                    id,
645                    anchor_id,
646                    anchor.layer,
647                    anchor.at[0],
648                    anchor.at[1]
649                ])?;
650            }
651            insert_stairwells(&connection, id, plane)?;
652            for (key, chunk) in &plane.chunks {
653                let (layer, cx, cy) = *key;
654                let ground = record_ground(world, plane, *key, chunk);
655                let record = chunk_record(chunk, &ground, &layers, &strings)?;
656                let data = encode_chunk(&record).map_err(|error| Error::Invalid(error.0))?;
657                chunks.execute(params![id, layer, cx, cy, CHUNK_ENCODING, data])?;
658                collect_uids(&mut uids, id, *key, chunk)?;
659            }
660        }
661        let mut object_uids = connection.prepare(
662            "INSERT INTO object_uids (uid, plane, layer, cx, cy, object) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
663        )?;
664        for (uid, (plane, layer, cx, cy, object)) in uids {
665            object_uids.execute(params![uid, plane, layer, cx, cy, object])?;
666        }
667        let mut definitions = connection
668            .prepare("INSERT INTO definitions (type, id, document) VALUES (?1, ?2, ?3)")?;
669        for ((kind, id), definition) in world.definitions() {
670            definitions.execute(params![kind, id, document(&definition.canonical)?])?;
671        }
672    }
673    connection.execute_batch("COMMIT; VACUUM;")?;
674    // The model is public, so the compiled pack is verified like any other
675    // before it is returned: a malformed model never yields an invalid pack.
676    let pack = Pack::compiled(connection)?;
677    // The compiler decodes only what it just encoded, so no budget applies.
678    pack.verify_contents(u64::MAX).map_err(|error| {
679        Error::Invalid(format!(
680            "the world does not compile to a valid pack: {error}"
681        ))
682    })?;
683    let connection = pack.into_connection();
684    let data = connection.serialize(MAIN_DB)?;
685    Ok(data.to_vec())
686}
687
688/// Whether `path` is a regular file holding exactly `data`. The length is
689/// compared first and the contents in fixed-size blocks, so a large file is
690/// never loaded whole.
691fn same_file_contents(path: &Path, data: &[u8]) -> Result<bool, Error> {
692    let metadata = std::fs::symlink_metadata(path)?;
693    if !metadata.is_file() || metadata.len() != u64::try_from(data.len()).unwrap_or(u64::MAX) {
694        return Ok(false);
695    }
696    let mut file = std::fs::File::open(path)?;
697    let mut buffer = vec![0; 64 * 1024];
698    let mut offset = 0;
699    loop {
700        let read = file.read(&mut buffer)?;
701        if read == 0 {
702            return Ok(offset == data.len());
703        }
704        match data.get(offset..offset + read) {
705            Some(expected) if expected == &buffer[..read] => offset += read,
706            _ => return Ok(false),
707        }
708    }
709}
710
711/// A pack written to disk.
712#[derive(Clone, Debug, Eq, PartialEq)]
713pub struct Written {
714    pub path: PathBuf,
715    /// SHA-256 of the pack file, which is also its name.
716    pub digest: [u8; 32],
717    pub bytes: u64,
718    /// `false` when an identical pack was already present.
719    pub created: bool,
720}
721
722/// Compiles a world and writes it to `directory/<sha256>.twpack`, creating
723/// the directory if needed. The write is atomic and never replaces a
724/// different file.
725///
726/// # Errors
727/// Returns an error if compilation or the write fails, or if a different
728/// file already has the pack's name.
729pub fn write_pack(world: &World, directory: &Path) -> Result<Written, Error> {
730    let data = compile(world)?;
731    std::fs::create_dir_all(directory)?;
732    let digest: [u8; 32] = Sha256::digest(&data).into();
733    let path = directory.join(format!("{}.twpack", hex(&digest)));
734    let bytes = u64::try_from(data.len()).unwrap_or(u64::MAX);
735    if std::fs::symlink_metadata(&path).is_ok() {
736        if same_file_contents(&path, &data)? {
737            return Ok(Written {
738                path,
739                digest,
740                bytes,
741                created: false,
742            });
743        }
744        return Err(Error::Invalid(format!(
745            "{} exists with different contents",
746            path.display()
747        )));
748    }
749    let mut temporary = tempfile::NamedTempFile::new_in(directory)?;
750    temporary.write_all(&data)?;
751    temporary.as_file().sync_all()?;
752    let created = persist(temporary, &path, &data)?;
753    Ok(Written {
754        path,
755        digest,
756        bytes,
757        created,
758    })
759}
760
761/// Moves `temporary` to `path` without replacing anything. When another
762/// writer got there first with the same bytes, that is not an error: returns
763/// whether this call created the file.
764pub(crate) fn persist(
765    temporary: tempfile::NamedTempFile,
766    path: &Path,
767    data: &[u8],
768) -> Result<bool, Error> {
769    match temporary.persist_noclobber(path) {
770        Ok(_) => Ok(true),
771        Err(error)
772            if error.error.kind() == std::io::ErrorKind::AlreadyExists
773                && same_file_contents(path, data)? =>
774        {
775            Ok(false)
776        }
777        Err(error) => Err(Error::Io(error.error)),
778    }
779}