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;