Skip to main content

tilewright_source/
lib.rs

1// Copyright 2026 Seraphina Studios
2// SPDX-License-Identifier: MIT
3
4//! Loads and validates a Tilewright world source tree
5//! (`docs/spec/FORMAT.md`) into a typed, game-neutral [`World`].
6//!
7//! Loading is bounded and fail-closed: only the files the format defines are
8//! read, symbolic links and unexpected files are errors, every document is
9//! parsed as strict JSONC under the format's limits, and every problem is
10//! reported as a [`Diagnostic`] with its file, byte span and a stable code.
11//! A world is returned only when there are no diagnostics at all.
12//!
13//! A profile's terrain transitions ([`transitions`]) derive edge, corner and
14//! variant tiles from painted terrain; [`Transitions::resolve_chunk`] is the
15//! one resolver the compiler, editors and other tools share.
16//!
17//! [`load_indexed_in`] also returns a [`ReferenceIndex`]: every reference
18//! the loader checks, such as an object's prototype or a zone's parent,
19//! with what it names, what holds it and its byte span, for finding what
20//! names something before it is removed or renamed ([`index`]). A
21//! [`World`] lists the files its revision covers ([`World::files`]).
22//!
23//! The root path is resolved as the caller gives it and may be a link;
24//! nothing beneath it is followed. On Unix every path component beneath the
25//! root is opened through its parent directory's handle without following
26//! symbolic links, so a link swapped into the tree during a load is never
27//! followed. Other platforms check each component by
28//! path before using it; that is a best effort which a local process changing
29//! the tree during the load can race. Race-free loading is Unix-only.
30//!
31//! ```no_run
32//! let world = tilewright_source::load(std::path::Path::new("content"))
33//!     .map_err(|diagnostics| diagnostics.len())?;
34//! println!("{} planes", world.planes().len());
35//! # Ok::<(), usize>(())
36//! ```
37
38mod access;
39mod chunk;
40mod defs;
41mod diagnostic;
42mod files;
43pub mod index;
44mod plane;
45mod profile;
46mod read;
47mod schema;
48mod stairwell;
49pub mod transitions;
50mod uid;
51
52use std::{collections::BTreeMap, path::Path, sync::Arc};
53use tilewright_jsonc::Limits;
54
55pub use access::{
56    Access, AccessError, Audience, LOCK_NAME, Mounts, SourceDirectory, SourceLock, SourceRoot,
57};
58use diagnostic::Diagnostics;
59pub use diagnostic::{Code, Diagnostic};
60pub use files::{SourceFile, is_jsonc_path};
61pub use index::ReferenceIndex;
62pub use read::{is_identifier, is_namespace};
63pub use schema::{FieldSpec, FieldType, MAXIMUM_FIELD_DEPTH, MAXIMUM_LIST_ITEMS, Schema};
64pub use transitions::{ResolvedChunk, TerrainSource, Transitions};
65
66/// Tiles along each side of a chunk in format 1.
67pub const CHUNK_SIZE: u16 = 32;
68
69/// Most ground layers a profile may declare.
70pub const MAXIMUM_GROUND_LAYERS: usize = 8;
71
72/// Largest `layer_height` a profile may declare. A stacked elevation,
73/// `layer × layer_height + height + z`, then stays within the format's
74/// integer range for every 32-bit layer, height and `z`.
75pub const MAXIMUM_LAYER_HEIGHT: u32 = 65_535;
76
77/// Bounds on what a load may read.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub struct LoadOptions {
80    /// Limits for every JSONC document. They can only tighten the format's
81    /// limits: a value above [`Limits::default`] is lowered to it.
82    pub limits: Limits,
83    /// Most files read in one load.
84    pub maximum_files: usize,
85    /// Most directory entries listed in one load, counted before they are
86    /// examined, a directory that holds no file, which the layout ignores,
87    /// as much as any. Publishing a transaction lists the directories on
88    /// the way to its files against as large a budget, `profile/` included,
89    /// so planning or publishing its undo can need one more entry for each
90    /// directory it made (`docs/spec/TRANSACTIONS.md` section 8.1).
91    pub maximum_entries: usize,
92    /// Most bytes read in one load.
93    pub maximum_total_bytes: u64,
94    /// Most diagnostics kept, at least one; past this, one `source.budget`
95    /// diagnostic notes that the rest were dropped.
96    pub maximum_diagnostics: usize,
97}
98
99impl Default for LoadOptions {
100    fn default() -> Self {
101        Self {
102            limits: Limits::default(),
103            maximum_files: 500_000,
104            maximum_entries: 1_000_000,
105            maximum_total_bytes: 8 * 1024 * 1024 * 1024,
106            maximum_diagnostics: 10_000,
107        }
108    }
109}
110
111/// A validated world source tree.
112///
113/// A `World` can only be produced by a load, such as [`load`] or
114/// [`load_with`], so every one has passed validation; its parts are read through accessors and cannot be
115/// changed in place.
116///
117/// ```compile_fail
118/// # fn planes(world: tilewright_source::World) {
119/// let planes = world.planes; // the fields are private
120/// # }
121/// ```
122#[derive(Clone, Debug, PartialEq)]
123pub struct World {
124    profile: Profile,
125    planes: BTreeMap<String, Plane>,
126    definitions: BTreeMap<(String, String), Definition>,
127    object_uids: BTreeMap<String, ObjectLocation>,
128    source_digest: [u8; 32],
129    files: BTreeMap<String, SourceFile>,
130}
131
132impl World {
133    #[must_use]
134    pub const fn profile(&self) -> &Profile {
135        &self.profile
136    }
137
138    /// Planes by ID.
139    #[must_use]
140    pub const fn planes(&self) -> &BTreeMap<String, Plane> {
141        &self.planes
142    }
143
144    /// Definitions keyed by `(type, id)`.
145    #[must_use]
146    pub const fn definitions(&self) -> &BTreeMap<(String, String), Definition> {
147        &self.definitions
148    }
149
150    /// SHA-256 over every file read, as defined in `docs/spec/PACK.md`.
151    #[must_use]
152    pub const fn source_digest(&self) -> [u8; 32] {
153        self.source_digest
154    }
155
156    /// Every file the load read, which [`World::source_digest`] covers, by
157    /// root-relative `/`-separated path, each with its length and SHA-256
158    /// as read. An overlay's files are listed as the overlay gave them. A
159    /// file the layout lets the loader ignore, such as one under
160    /// `profile/` that the profile does not name, is not read, so it is
161    /// not listed, and changing it does not change the digest.
162    #[must_use]
163    pub const fn files(&self) -> &BTreeMap<String, SourceFile> {
164        &self.files
165    }
166
167    /// Every object uid of the world, with where its object is. Uids are
168    /// unique in the whole world, so a uid alone names one object.
169    #[must_use]
170    pub const fn object_uids(&self) -> &BTreeMap<String, ObjectLocation> {
171        &self.object_uids
172    }
173
174    /// Where the object with `uid` is.
175    #[must_use]
176    pub fn object_location(&self, uid: &str) -> Option<&ObjectLocation> {
177        self.object_uids.get(uid)
178    }
179
180    /// The object with `uid`.
181    #[must_use]
182    pub fn object(&self, uid: &str) -> Option<&Object> {
183        let location = self.object_uids.get(uid)?;
184        self.planes
185            .get(&location.plane)?
186            .chunks
187            .get(&location.chunk)?
188            .object(&location.path)
189    }
190}
191
192/// Where an object is: its chunk, and its place in the chunk.
193#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
194pub struct ObjectLocation {
195    pub plane: String,
196    /// The chunk's key in [`Plane::chunks`]: `(layer, cx, cy)`.
197    pub chunk: (i32, i64, i64),
198    /// The object's index in the chunk's `objects`, then, for an inventory
199    /// entry, its index in the inventory of each object down to it. A
200    /// top-level object's path has one index.
201    pub path: Vec<usize>,
202}
203
204/// The game profile.
205#[derive(Clone, Debug, PartialEq)]
206pub struct Profile {
207    pub id: String,
208    pub version: String,
209    pub projection: Projection,
210    pub render_layers: Vec<String>,
211    /// Ground layers in profile order; each chunk stores one grid per entry.
212    pub ground_layers: Vec<String>,
213    /// Schemas by definition type. Roles that name the same schema path
214    /// share one schema.
215    pub definition_types: BTreeMap<String, Arc<Schema>>,
216    pub object_fields: Arc<Schema>,
217    pub zone_properties: Arc<Schema>,
218    /// Height units per layer: layer `L` sits at `L × layer_height`, and
219    /// tile heights stay relative to their layer. `None` when the profile
220    /// does not stack layers.
221    pub layer_height: Option<u32>,
222    pub movement: Option<Movement>,
223    /// The terrain transitions the profile names (`docs/spec/FORMAT.md`
224    /// section 13), which derive edge, corner and variant tiles from
225    /// painted terrain.
226    pub transitions: Option<Arc<Transitions>>,
227    /// Canonical JSON of the profile document.
228    pub canonical: String,
229}
230
231impl Profile {
232    /// The position of a render layer in the profile's order.
233    #[must_use]
234    pub fn render_layer_index(&self, name: &str) -> Option<usize> {
235        self.render_layers.iter().position(|layer| layer == name)
236    }
237}
238
239#[derive(Clone, Copy, Debug, Eq, PartialEq)]
240pub enum Projection {
241    Isometric { tile_width: u32, tile_height: u32 },
242    TopDown { tile_size: u32 },
243}
244
245/// Movement analysis rules for the editor; the format does not interpret them.
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
247pub struct Movement {
248    pub max_step: u32,
249    pub fall_unit: u32,
250}
251
252#[derive(Clone, Copy, Debug, Eq, PartialEq)]
253pub enum PlaneKind {
254    Shared,
255    Instanced,
256    PerPlayer,
257}
258
259impl PlaneKind {
260    #[must_use]
261    pub const fn as_str(self) -> &'static str {
262        match self {
263            Self::Shared => "shared",
264            Self::Instanced => "instanced",
265            Self::PerPlayer => "per_player",
266        }
267    }
268}
269
270/// One plane with its zones, anchors, stairwells and chunks.
271#[derive(Clone, Debug, PartialEq)]
272pub struct Plane {
273    pub id: String,
274    pub title: Option<String>,
275    pub kind: PlaneKind,
276    pub chunk_size: u16,
277    pub default_zone: Option<String>,
278    /// Canonical JSON of `plane.jsonc`.
279    pub canonical: String,
280    pub zones: BTreeMap<String, Zone>,
281    pub anchors: BTreeMap<String, Anchor>,
282    pub stairwells: BTreeMap<String, Stairwell>,
283    /// Chunks keyed by `(layer, cx, cy)`.
284    pub chunks: BTreeMap<(i32, i64, i64), Chunk>,
285}
286
287#[derive(Clone, Debug, PartialEq)]
288pub struct Zone {
289    pub id: String,
290    pub parent: Option<String>,
291    pub name: Option<String>,
292    /// Canonical JSON of the zone entry.
293    pub canonical: String,
294}
295
296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297pub struct Anchor {
298    pub layer: i32,
299    pub at: [i64; 2],
300}
301
302/// One exit spanning several layers: steps on adjacent layers, used end to
303/// end in one move.
304#[derive(Clone, Debug, Eq, PartialEq)]
305pub struct Stairwell {
306    pub id: String,
307    /// Steps in source order, at least two, each on the layer next to the
308    /// one before it, in one direction, and at most one tile away on each
309    /// axis.
310    pub steps: Vec<StairwellStep>,
311    pub direction: StairwellDirection,
312    /// Whether consumers draw only the step on the viewer's layer.
313    pub hidden_on_other_layers: bool,
314    /// Canonical JSON of the `custom` object.
315    pub custom: Option<String>,
316    /// Canonical JSON of the stairwell entry.
317    pub canonical: String,
318}
319
320impl Stairwell {
321    /// Whether `steps` form a stairwell: at least two, each on the layer
322    /// next to the one before it, all going up or all going down, and at
323    /// most one tile away from the step before it on each axis.
324    #[must_use]
325    pub fn connects(steps: &[StairwellStep]) -> bool {
326        let [first, second, ..] = steps else {
327            return false;
328        };
329        let up = second.layer > first.layer;
330        steps.windows(2).all(|pair| pair[0].leads_to(&pair[1], up))
331    }
332
333    /// The step on the lowest layer.
334    #[must_use]
335    pub fn lower_end(&self) -> Option<&StairwellStep> {
336        self.steps.iter().min_by_key(|step| step.layer)
337    }
338
339    /// The step on the highest layer.
340    #[must_use]
341    pub fn upper_end(&self) -> Option<&StairwellStep> {
342        self.steps.iter().max_by_key(|step| step.layer)
343    }
344}
345
346/// One step of a stairwell, in plane coordinates.
347#[derive(Clone, Debug, Eq, PartialEq)]
348pub struct StairwellStep {
349    pub layer: i32,
350    pub at: [i64; 2],
351    pub proto: String,
352}
353
354impl StairwellStep {
355    /// Whether `next` may follow this step in a stairwell that goes `up`
356    /// (or down): it is on the next layer that way and at most one tile
357    /// away on each axis.
358    #[must_use]
359    pub fn leads_to(&self, next: &Self, up: bool) -> bool {
360        adjacent((self.layer, self.at), (next.layer, next.at), up)
361    }
362}
363
364/// Whether a step at position `next` (layer and plane coordinates) may
365/// follow one at `before` in a stairwell that goes `up` (or down).
366pub(crate) fn adjacent(before: (i32, [i64; 2]), next: (i32, [i64; 2]), up: bool) -> bool {
367    let change = if up { 1 } else { -1 };
368    i64::from(next.0) - i64::from(before.0) == change
369        && before
370            .1
371            .iter()
372            .zip(next.1)
373            .all(|(before, after)| (i128::from(*before) - i128::from(after)).abs() <= 1)
374}
375
376/// Which way a stairwell may be used.
377#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
378pub enum StairwellDirection {
379    /// From either end to the other.
380    #[default]
381    Both,
382    /// Only from the upper end to the lower end.
383    Down,
384    /// Only from the lower end to the upper end.
385    Up,
386}
387
388impl StairwellDirection {
389    #[must_use]
390    pub const fn as_str(self) -> &'static str {
391        match self {
392            Self::Both => "both",
393            Self::Down => "down",
394            Self::Up => "up",
395        }
396    }
397
398    /// The direction a source or pack spells `text`.
399    #[must_use]
400    pub fn parse(text: &str) -> Option<Self> {
401        match text {
402            "both" => Some(Self::Both),
403            "down" => Some(Self::Down),
404            "up" => Some(Self::Up),
405            _ => None,
406        }
407    }
408}
409
410/// A character grid: one palette value per tile.
411#[derive(Clone, Debug, Default, Eq, PartialEq)]
412pub struct Grid {
413    /// Values in palette declaration order.
414    pub palette: Vec<String>,
415    /// `chunk_size²` cells, row-major; a cell is a palette index plus one,
416    /// or 0 for an empty tile.
417    pub cells: Vec<u16>,
418}
419
420impl Grid {
421    /// The value of a cell, or `None` for an empty tile.
422    #[must_use]
423    pub fn value(&self, index: usize) -> Option<&str> {
424        let cell = usize::from(*self.cells.get(index)?);
425        cell.checked_sub(1)
426            .and_then(|palette| self.palette.get(palette))
427            .map(String::as_str)
428    }
429}
430
431/// One chunk of one layer.
432#[derive(Clone, Debug, PartialEq)]
433pub struct Chunk {
434    /// Source path, relative to the root.
435    pub path: String,
436    /// One grid per profile ground layer, in profile order.
437    pub ground: Vec<Grid>,
438    /// `chunk_size²` tile heights, row-major.
439    pub heights: Vec<i32>,
440    /// Zone per tile; empty cells take the plane's default zone.
441    pub zones: Grid,
442    pub objects: Vec<Object>,
443}
444
445impl Chunk {
446    /// The object at `path`: an index into `objects`, then one into the
447    /// inventory of each object down to it, as in [`ObjectLocation::path`].
448    #[must_use]
449    pub fn object(&self, path: &[usize]) -> Option<&Object> {
450        let (first, rest) = path.split_first()?;
451        rest.iter()
452            .try_fold(self.objects.get(*first)?, |object, index| {
453                object.inventory.get(*index)
454            })
455    }
456
457    /// Every object of the chunk, inventory entries included, in
458    /// depth-first order: each object comes before its inventory entries,
459    /// and those before the object after it. An object's position in this
460    /// order is its index in the chunk (`docs/spec/FORMAT.md` section 7).
461    #[must_use]
462    pub fn depth_first(&self) -> DepthFirst<'_> {
463        DepthFirst {
464            levels: vec![self.objects.iter()],
465        }
466    }
467}
468
469/// The objects of a chunk in depth-first order; see [`Chunk::depth_first`].
470#[derive(Clone, Debug)]
471pub struct DepthFirst<'c> {
472    /// The objects left at each depth, outermost first.
473    levels: Vec<std::slice::Iter<'c, Object>>,
474}
475
476impl<'c> Iterator for DepthFirst<'c> {
477    type Item = &'c Object;
478
479    fn next(&mut self) -> Option<Self::Item> {
480        loop {
481            let level = self.levels.last_mut()?;
482            if let Some(object) = level.next() {
483                self.levels.push(object.inventory.iter());
484                return Some(object);
485            }
486            self.levels.pop();
487        }
488    }
489}
490
491/// A placed object or an inventory entry.
492#[derive(Clone, Debug, PartialEq)]
493pub struct Object {
494    /// The object's stable ID, unique in the world.
495    pub uid: Option<String>,
496    /// Chunk-local tile; `None` for inventory entries.
497    pub at: Option<[u16; 2]>,
498    pub proto: String,
499    pub layer: Option<String>,
500    pub sub_layer: i16,
501    pub z: i32,
502    pub fields: Vec<(String, FieldValue)>,
503    pub portal: Option<Portal>,
504    pub inventory: Vec<Object>,
505    /// Canonical JSON of the `custom` object.
506    pub custom: Option<String>,
507}
508
509/// A field value. Arrays and objects, the values of `json`, `list` and
510/// `object` fields, are kept as canonical JSON.
511#[derive(Clone, Debug, PartialEq)]
512pub enum FieldValue {
513    Null,
514    Bool(bool),
515    Integer(i64),
516    Number(f64),
517    String(String),
518    Json(String),
519}
520
521#[derive(Clone, Debug, Eq, PartialEq)]
522pub enum Portal {
523    Location {
524        plane: Option<String>,
525        layer: i32,
526        at: [i64; 2],
527    },
528    Anchor {
529        plane: Option<String>,
530        anchor: String,
531    },
532}
533
534#[derive(Clone, Debug, PartialEq)]
535pub struct Definition {
536    pub fields: Vec<(String, FieldValue)>,
537    /// Canonical JSON of the definition document.
538    pub canonical: String,
539}
540
541/// Loads a source tree with default bounds.
542///
543/// # Errors
544/// Returns every diagnostic found, sorted by path and position.
545pub fn load(root: &Path) -> Result<World, Vec<Diagnostic>> {
546    load_with(root, &LoadOptions::default())
547}
548
549/// Loads a source tree.
550///
551/// # Errors
552/// Returns every diagnostic found, sorted by path and position.
553pub fn load_with(root: &Path, options: &LoadOptions) -> Result<World, Vec<Diagnostic>> {
554    load_edited(root, options, &Overlay::new())
555}
556
557/// Edits applied over the files of a tree for [`load_edited`], keyed by
558/// root-relative `/`-separated path: `Some(text)` replaces or creates the
559/// file, and `None` removes it.
560pub type Overlay = BTreeMap<String, Option<String>>;
561
562/// Loads a source tree as it would be with `overlay` applied, without
563/// writing anything. Overlay files are read under the same limits and
564/// budget as files on disk, and a directory that holds one exists. One
565/// that the overlay leaves without a file holds none, and is ignored where
566/// the layout ignores such a directory (FORMAT.md section 2). An overlay of
567/// more entries than `maximum_entries` is refused before anything is read.
568///
569/// # Errors
570/// Returns every diagnostic found, sorted by path and position.
571pub fn load_edited(
572    root: &Path,
573    options: &LoadOptions,
574    overlay: &Overlay,
575) -> Result<World, Vec<Diagnostic>> {
576    load_edited_in(&SourceRoot::new(root), options, overlay)
577}
578
579/// Loads the tree of an opened root as [`load_edited`] does, through the
580/// handle rather than its path, so that what is validated is the directory
581/// the caller opened.
582///
583/// # Errors
584/// Returns every diagnostic found, sorted by path and position.
585pub fn load_edited_in(
586    root: &SourceRoot,
587    options: &LoadOptions,
588    overlay: &Overlay,
589) -> Result<World, Vec<Diagnostic>> {
590    load_recording(root, options, overlay, false).map(|(world, _)| world)
591}
592
593/// Loads the tree of an opened root as [`load_edited_in`] does, and also
594/// returns the index of its references: every reference the loader
595/// checks or follows, with its target, role, holder, file and span (see
596/// [`index`]). The world, and on failure the diagnostics, are those
597/// [`load_edited_in`] gives, with one exception: a tree whose index would
598/// be too large to number is refused (see below).
599///
600/// Indexing costs memory in proportion to the references, about 36 bytes
601/// each plus their distinct strings, and some time to sort them; the
602/// other loads record nothing. Recording stops at the load's first
603/// diagnostic, as the load then fails, so a broken tree costs about what
604/// a plain load of it does. A tree with more references, distinct strings
605/// or holders than 32 bits number is refused with a `source.budget`
606/// diagnostic, which only an indexed load gives.
607///
608/// ```no_run
609/// use tilewright_source::{LoadOptions, Overlay, SourceRoot, index::Target};
610///
611/// let root = SourceRoot::new(std::path::Path::new("content"));
612/// let (_, index) = tilewright_source::load_indexed_in(&root, &LoadOptions::default(), &Overlay::new())
613///     .map_err(|diagnostics| diagnostics.len())?;
614/// for reference in index.to(Target::Definition { kind: "proto", id: "tree" }) {
615///     println!("{} {:?} {:?}", reference.file(), reference.span(), reference.role());
616/// }
617/// # Ok::<(), usize>(())
618/// ```
619///
620/// # Errors
621/// Returns every diagnostic found, sorted by path and position, as
622/// [`load_edited_in`] does; or, for a tree that otherwise loads but whose
623/// references, distinct strings or holders are too many to number, one
624/// `source.budget` diagnostic at `.`.
625pub fn load_indexed_in(
626    root: &SourceRoot,
627    options: &LoadOptions,
628    overlay: &Overlay,
629) -> Result<(World, ReferenceIndex), Vec<Diagnostic>> {
630    load_recording(root, options, overlay, true).map(|(world, recorder)| {
631        (
632            world,
633            recorder.map(index::Recorder::finish).unwrap_or_default(),
634        )
635    })
636}
637
638/// Loads the tree, recording its references when `record` is set.
639fn load_recording(
640    root: &SourceRoot,
641    options: &LoadOptions,
642    overlay: &Overlay,
643    record: bool,
644) -> Result<(World, Option<index::Recorder>), Vec<Diagnostic>> {
645    // At least one diagnostic is kept, so a failed load always says why.
646    let options = &LoadOptions {
647        limits: within_format(options.limits),
648        maximum_diagnostics: options.maximum_diagnostics.max(1),
649        ..*options
650    };
651    // Each entry adds, replaces or removes a directory entry; more than a
652    // load may list cannot load, and is not looked at.
653    if overlay.len() > options.maximum_entries {
654        return Err(vec![Diagnostic::file(
655            ".",
656            Code::Budget,
657            format!(
658                "the edits hold more than {} entries",
659                options.maximum_entries
660            ),
661        )]);
662    }
663    // Every key must be a path a file could have, so no entry costs more
664    // than a path does.
665    if let Some(key) = overlay
666        .keys()
667        .find(|key| key.is_empty() || access::checked(key).is_err())
668    {
669        let shown = if key.len() <= 128 {
670            format!("{key:?}")
671        } else {
672            format!("a path of {} bytes", key.len())
673        };
674        return Err(vec![Diagnostic::file(
675            ".",
676            Code::Path,
677            format!("the edits name {shown}, which is not a path a file of the tree can have"),
678        )]);
679    }
680    let mut diagnostics = Diagnostics::new(options.maximum_diagnostics);
681    let mut tree = files::Tree::in_root(root.platform(), options, overlay, diagnostics.total());
682    let mut references = schema::References::new(record, diagnostics.total());
683    let world = load_tree(&mut tree, &mut references, &mut diagnostics);
684    diagnostics.append(tree.take_diagnostics());
685    match world {
686        Some(world) if diagnostics.is_empty() => {
687            let recorder = references.into_recorder();
688            // An index that could not hold every reference is refused,
689            // never cut short.
690            if recorder.as_ref().is_some_and(index::Recorder::overflowed) {
691                return Err(vec![Diagnostic::file(
692                    ".",
693                    Code::Budget,
694                    "the tree has more references, strings or holders than an index can number",
695                )]);
696            }
697            Ok((
698                World {
699                    source_digest: tree.digest(),
700                    files: tree.take_files(),
701                    ..world
702                },
703                recorder,
704            ))
705        }
706        _ => Err(diagnostics.finish()),
707    }
708}
709
710/// `limits`, each lowered to the format's own limit where it is higher.
711fn within_format(limits: Limits) -> Limits {
712    let format = Limits::default();
713    Limits {
714        max_input_bytes: limits.max_input_bytes.min(format.max_input_bytes),
715        max_depth: limits.max_depth.min(format.max_depth),
716        max_values: limits.max_values.min(format.max_values),
717        max_array_items: limits.max_array_items.min(format.max_array_items),
718        max_object_keys: limits.max_object_keys.min(format.max_object_keys),
719        max_string_bytes: limits.max_string_bytes.min(format.max_string_bytes),
720        max_number_bytes: limits.max_number_bytes.min(format.max_number_bytes),
721        max_comments: limits.max_comments.min(format.max_comments),
722        max_comment_bytes: limits.max_comment_bytes.min(format.max_comment_bytes),
723        max_lines: limits.max_lines.min(format.max_lines),
724        max_line_bytes: limits.max_line_bytes.min(format.max_line_bytes),
725    }
726}
727
728fn load_tree(
729    tree: &mut files::Tree,
730    references: &mut schema::References,
731    diagnostics: &mut Diagnostics,
732) -> Option<World> {
733    let (manifest_path, plane_ids) = profile::read_manifest(tree, references, diagnostics)?;
734    let (mut profile, transitions_path) =
735        profile::read_profile(tree, &manifest_path, references, diagnostics)?;
736    let definitions = defs::read_definitions(tree, &profile, references, diagnostics);
737    // A transitions document that fails has reported why, so the load
738    // fails with it.
739    if let Some(path) = transitions_path {
740        profile.transitions =
741            transitions::read_transitions(tree, &path, &profile, references, diagnostics)
742                .map(Arc::new);
743    }
744    let mut planes = BTreeMap::new();
745    for plane in plane::read_planes(tree, &profile, &plane_ids, references, diagnostics) {
746        planes.insert(plane.id.clone(), plane);
747    }
748    let object_uids = uid::index(&planes);
749    Some(World {
750        profile,
751        planes,
752        definitions,
753        object_uids,
754        source_digest: [0; 32],
755        files: BTreeMap::new(),
756    })
757}
758
759#[cfg(test)]
760mod tests;