1use 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
27pub const COMPILER: &str = concat!("tilewright-pack ", env!("CARGO_PKG_VERSION"));
29
30pub(crate) struct Strings<'w>(BTreeMap<&'w str, u32>);
35
36pub(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
48pub(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
57pub(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
68pub(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 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
150fn 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
161pub(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
169pub(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
185pub(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
221pub(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
234pub(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
255fn 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
289pub(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
386fn 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
401fn 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 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 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
466pub(crate) type UidRows<'w> = BTreeMap<&'w str, (&'w str, i32, i64, i64, i64)>;
470
471pub(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 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
493pub(crate) type TileStep<'w> = (&'w str, i64, &'w str);
496
497pub(crate) fn stairwell_tiles(
503 plane: &tilewright_source::Plane,
504) -> Result<BTreeMap<(i32, i64, i64), TileStep<'_>>, Error> {
505 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
547fn insert_stairwells(
549 connection: &Connection,
550 id: &str,
551 plane: &tilewright_source::Plane,
552) -> Result<(), Error> {
553 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
577pub 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 let pack = Pack::compiled(connection)?;
677 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
688fn 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#[derive(Clone, Debug, Eq, PartialEq)]
713pub struct Written {
714 pub path: PathBuf,
715 pub digest: [u8; 32],
717 pub bytes: u64,
718 pub created: bool,
720}
721
722pub 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
761pub(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}