1use crate::MapError;
19use crate::model::Product;
20use serde::Serialize;
21use serde::ser::{SerializeMap, SerializeSeq};
22use std::collections::BTreeMap;
23use std::fs::{self, File, OpenOptions};
24use std::io::Write;
25use std::path::{Path, PathBuf};
26
27pub const PARTIAL_SUFFIX: &str = ".partial";
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, PartialOrd, Ord)]
32pub struct Rect {
33 pub col: usize,
34 pub row: usize,
35 pub width: usize,
36 pub height: usize,
37}
38
39impl Rect {
40 pub fn intersect(&self, o: &Rect) -> Option<Rect> {
41 let c0 = self.col.max(o.col);
42 let r0 = self.row.max(o.row);
43 let c1 = (self.col + self.width).min(o.col + o.width);
44 let r1 = (self.row + self.height).min(o.row + o.height);
45 (c0 < c1 && r0 < r1).then(|| Rect {
46 col: c0,
47 row: r0,
48 width: c1 - c0,
49 height: r1 - r0,
50 })
51 }
52 pub fn pixels(&self) -> usize {
53 self.width * self.height
54 }
55}
56
57pub struct TileOut {
59 pub profile: usize,
60 pub tile: (usize, usize),
61 pub rect: Rect,
62 pub raw_wse: Vec<f64>,
63 pub wse: Vec<f64>,
64 pub depth: Vec<f64>,
65 pub ground: Vec<f64>,
66 pub simplex_id: Vec<i64>,
67}
68
69#[derive(Clone)]
71pub struct TileRecord {
72 tile_row: usize,
73 tile_col: usize,
74 rect: Rect,
75 fnv1a64: [u64; 5],
76 raw_wse_valid: u64,
77 wet: u64,
78}
79
80pub const TILE_RECORD_BYTES: u64 = std::mem::size_of::<TileRecord>() as u64;
82
83#[derive(Serialize)]
86struct TileRecordJson {
87 tile_row: usize,
88 tile_col: usize,
89 rect: Rect,
90 fnv1a64: BTreeMap<&'static str, String>,
91 raw_wse_valid: u64,
92 wet: u64,
93}
94
95struct UnitState {
96 dir: PathBuf,
97 window: Rect,
98 header: serde_json::Value,
99 files: Vec<(Product, File)>,
100 tiles: Vec<TileRecord>,
101 bytes: u64,
102}
103
104pub struct DirSink {
105 root: PathBuf,
106 products: Vec<Product>,
107 units: BTreeMap<(usize, usize), UnitState>,
109 pub bytes_written: u64,
110 pub tiles_written: u64,
111 pub unit_files_written: u64,
112 pub dir_sync_unsupported: u64,
114}
115
116pub fn fnv1a64(bytes: &[u8]) -> u64 {
117 let mut h: u64 = 0xcbf29ce484222325;
118 for b in bytes {
119 h ^= *b as u64;
120 h = h.wrapping_mul(0x100000001b3);
121 }
122 h
123}
124
125#[cfg(unix)]
126fn write_at(f: &File, buf: &[u8], off: u64) -> std::io::Result<()> {
127 use std::os::unix::fs::FileExt;
128 f.write_all_at(buf, off)
129}
130
131#[cfg(windows)]
132fn write_at(f: &File, mut buf: &[u8], mut off: u64) -> std::io::Result<()> {
133 use std::os::windows::fs::FileExt;
134 while !buf.is_empty() {
135 let n = f.seek_write(buf, off)?;
136 if n == 0 {
137 return Err(std::io::Error::other("short positioned write"));
138 }
139 buf = &buf[n..];
140 off += n as u64;
141 }
142 Ok(())
143}
144
145fn io_at(path: &Path, e: std::io::Error) -> MapError {
146 MapError::Io(format!("{}: {e}", path.display()))
147}
148
149fn partial(path: &Path) -> PathBuf {
150 let mut s = path.as_os_str().to_os_string();
151 s.push(PARTIAL_SUFFIX);
152 PathBuf::from(s)
153}
154
155fn sync_dir(dir: &Path) -> Result<bool, MapError> {
158 #[cfg(unix)]
159 {
160 let r = File::open(dir).and_then(|d| d.sync_all());
161 match r {
162 Ok(()) => Ok(true),
163 Err(e)
164 if e.kind() == std::io::ErrorKind::InvalidInput
165 || e.kind() == std::io::ErrorKind::Unsupported =>
166 {
167 Ok(false)
168 }
169 Err(e) => Err(io_at(dir, e)),
170 }
171 }
172 #[cfg(not(unix))]
173 {
174 let _ = dir;
175 Ok(false)
176 }
177}
178
179fn write_atomic(
181 path: &Path,
182 fill: impl FnOnce(&mut std::io::BufWriter<&File>) -> Result<(), MapError>,
183) -> Result<(), MapError> {
184 let tmp = partial(path);
185 let result = (|| {
186 let f = File::create(&tmp).map_err(|e| io_at(&tmp, e))?;
187 {
188 let mut w = std::io::BufWriter::new(&f);
189 fill(&mut w)?;
190 w.flush().map_err(|e| io_at(&tmp, e))?;
191 }
192 f.sync_all().map_err(|e| io_at(&tmp, e))?;
193 drop(f);
194 fs::rename(&tmp, path).map_err(|e| io_at(path, e))
195 })();
196 if result.is_err() {
197 let _ = fs::remove_file(&tmp);
198 }
199 result
200}
201
202pub fn write_json(path: &Path, v: &serde_json::Value) -> Result<(), MapError> {
204 write_atomic(path, |w| {
205 serde_json::to_writer_pretty(w, v)
206 .map_err(|e| MapError::Io(format!("{}: {e}", path.display())))
207 })
208}
209
210struct UnitDoc<'a> {
214 doc: &'a serde_json::Map<String, serde_json::Value>,
215 tiles: &'a [TileRecord],
216 names: &'a [&'static str],
217}
218
219struct TilesSeq<'a> {
220 tiles: &'a [TileRecord],
221 names: &'a [&'static str],
222}
223
224impl Serialize for TilesSeq<'_> {
225 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
226 let mut seq = s.serialize_seq(Some(self.tiles.len()))?;
227 for t in self.tiles {
228 let rec = TileRecordJson {
229 tile_row: t.tile_row,
230 tile_col: t.tile_col,
231 rect: t.rect,
232 fnv1a64: self
233 .names
234 .iter()
235 .zip(t.fnv1a64)
236 .map(|(n, h)| (*n, format!("{h:016x}")))
237 .collect(),
238 raw_wse_valid: t.raw_wse_valid,
239 wet: t.wet,
240 };
241 let v = serde_json::to_value(&rec).map_err(serde::ser::Error::custom)?;
243 seq.serialize_element(&v)?;
244 }
245 seq.end()
246 }
247}
248
249impl Serialize for UnitDoc<'_> {
250 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
251 let mut m = s.serialize_map(Some(self.doc.len()))?;
252 for (k, v) in self.doc {
253 if k == "tiles" {
254 m.serialize_entry(
255 k,
256 &TilesSeq {
257 tiles: self.tiles,
258 names: self.names,
259 },
260 )?;
261 } else {
262 m.serialize_entry(k, v)?;
263 }
264 }
265 m.end()
266 }
267}
268
269impl DirSink {
270 pub fn create(out: &Path, products: &[Product]) -> Result<Self, MapError> {
272 fs::create_dir(out).map_err(|e| {
273 MapError::Io(format!(
274 "{}: {e} (output directory must be new)",
275 out.display()
276 ))
277 })?;
278 fs::create_dir(out.join("units")).map_err(|e| io_at(&out.join("units"), e))?;
279 let mut products = products.to_vec();
280 products.sort();
281 if products.len() > 5 {
282 return Err(MapError::Internal("more than five products".into()));
283 }
284 Ok(Self {
285 root: out.to_path_buf(),
286 products,
287 units: BTreeMap::new(),
288 bytes_written: 0,
289 tiles_written: 0,
290 unit_files_written: 0,
291 dir_sync_unsupported: 0,
292 })
293 }
294
295 pub fn unit_dir(&self, profile_id: &str, aoi_id: &str) -> PathBuf {
296 self.root.join("units").join(profile_id).join(aoi_id)
297 }
298
299 fn data_path(dir: &Path, p: Product) -> PathBuf {
300 dir.join(format!("{}.bin", p.name()))
301 }
302
303 pub fn begin_unit(
306 &mut self,
307 key: (usize, usize),
308 profile_id: &str,
309 aoi_id: &str,
310 window: Option<Rect>,
311 header: &serde_json::Value,
312 expected_tiles: usize,
313 ) -> Result<(), MapError> {
314 let dir = self.unit_dir(profile_id, aoi_id);
315 fs::create_dir_all(&dir).map_err(|e| io_at(&dir, e))?;
316 let Some(window) = window else {
317 let mut doc = header.clone();
318 doc["status"] = "empty_window".into();
319 return write_json(&dir.join("unit.json"), &doc);
320 };
321 let mut marker = header.clone();
323 marker["status"] = "in_progress".into();
324 write_json(&dir.join("unit.json"), &marker)?;
325 let mut tiles = Vec::new();
326 tiles
327 .try_reserve_exact(expected_tiles)
328 .map_err(|e| MapError::Budget(format!("tile records for {}: {e}", dir.display())))?;
329 self.units.insert(
331 key,
332 UnitState {
333 dir: dir.clone(),
334 window,
335 header: header.clone(),
336 files: Vec::new(),
337 tiles,
338 bytes: 0,
339 },
340 );
341 let u = self.units.get_mut(&key).expect("just inserted");
342 for p in &self.products {
343 let path = partial(&Self::data_path(&dir, *p));
344 let f = OpenOptions::new()
345 .write(true)
346 .create_new(true)
347 .open(&path)
348 .map_err(|e| io_at(&path, e))?;
349 u.files.push((*p, f));
350 let (_, f) = u.files.last().expect("just pushed");
351 f.set_len(window.pixels() as u64 * 8)
352 .map_err(|e| io_at(&path, e))?;
353 }
354 Ok(())
355 }
356
357 pub fn write_tile(&mut self, t: &TileOut, aoi_count: usize) -> Result<(), MapError> {
359 let mut row_buf: Vec<u8> = Vec::with_capacity(t.rect.width * 8);
360 for aoi in 0..aoi_count {
361 let Some(u) = self.units.get_mut(&(t.profile, aoi)) else {
362 continue;
363 };
364 let Some(ix) = u.window.intersect(&t.rect) else {
365 continue;
366 };
367 let mut rec = TileRecord {
368 tile_row: t.tile.0,
369 tile_col: t.tile.1,
370 rect: ix,
371 fnv1a64: [0; 5],
372 raw_wse_valid: 0,
373 wet: 0,
374 };
375 for r in ix.row..ix.row + ix.height {
376 let src = (r - t.rect.row) * t.rect.width + (ix.col - t.rect.col);
377 for c in 0..ix.width {
378 rec.raw_wse_valid +=
379 (!t.raw_wse.is_empty() && !t.raw_wse[src + c].is_nan()) as u64;
380 rec.wet += (!t.wse.is_empty() && !t.wse[src + c].is_nan()) as u64;
381 }
382 }
383 for (pi, (p, f)) in u.files.iter().enumerate() {
384 let mut hash: u64 = 0xcbf29ce484222325;
385 for r in ix.row..ix.row + ix.height {
386 let src = (r - t.rect.row) * t.rect.width + (ix.col - t.rect.col);
387 row_buf.clear();
388 match p {
389 Product::SimplexId => {
390 for v in &t.simplex_id[src..src + ix.width] {
391 row_buf.extend_from_slice(&v.to_le_bytes());
392 }
393 }
394 _ => {
395 let s = match p {
396 Product::RawWse => &t.raw_wse,
397 Product::Wse => &t.wse,
398 Product::Depth => &t.depth,
399 _ => &t.ground,
400 };
401 for v in &s[src..src + ix.width] {
402 row_buf.extend_from_slice(&v.to_le_bytes());
403 }
404 }
405 }
406 for b in &row_buf {
407 hash ^= *b as u64;
408 hash = hash.wrapping_mul(0x100000001b3);
409 }
410 let off = (((r - u.window.row) * u.window.width + (ix.col - u.window.col)) * 8)
411 as u64;
412 write_at(f, &row_buf, off)
413 .map_err(|e| io_at(&partial(&Self::data_path(&u.dir, *p)), e))?;
414 u.bytes += row_buf.len() as u64;
415 self.bytes_written += row_buf.len() as u64;
416 }
417 rec.fnv1a64[pi] = hash;
418 }
419 u.tiles.push(rec);
420 }
421 self.tiles_written += 1;
422 Ok(())
423 }
424
425 fn fail_unit(u: UnitState, e: &MapError, products: &[Product]) -> Vec<String> {
427 let mut problems = Vec::new();
428 drop(u.files);
429 for p in products {
430 let fin = Self::data_path(&u.dir, *p);
431 for path in [partial(&fin), fin] {
432 match fs::remove_file(&path) {
433 Ok(()) => {}
434 Err(x) if x.kind() == std::io::ErrorKind::NotFound => {}
435 Err(x) => problems.push(format!("remove {}: {x}", path.display())),
436 }
437 }
438 }
439 let mut doc = u.header;
440 doc["status"] = "failed".into();
441 doc["error"] = serde_json::json!({"kind": e.kind(), "message": e.to_string()});
442 if let Err(x) = write_json(&u.dir.join("unit.json"), &doc) {
443 problems.push(format!("failed-status unit.json: {x}"));
444 }
445 problems
446 }
447
448 pub fn end_unit(
451 &mut self,
452 key: (usize, usize),
453 header: &serde_json::Value,
454 error: Option<&MapError>,
455 ) -> Result<(), MapError> {
456 let Some(mut u) = self.units.remove(&key) else {
457 return Ok(());
458 };
459 u.header = header.clone();
460 if let Some(e) = error {
461 let problems = Self::fail_unit(u, e, &self.products);
462 return match problems.is_empty() {
463 true => Ok(()),
464 false => Err(MapError::Io(format!(
465 "cleanup of failed unit: {}",
466 problems.join("; ")
467 ))),
468 };
469 }
470 match self.commit(&mut u) {
471 Ok(()) => {
472 self.unit_files_written += self.products.len() as u64;
473 Ok(())
474 }
475 Err(e) => {
476 let problems = Self::fail_unit(u, &e, &self.products);
477 Err(match problems.is_empty() {
478 true => e,
479 false => MapError::Io(format!("{e}; cleanup: {}", problems.join("; "))),
480 })
481 }
482 }
483 }
484
485 fn commit(&mut self, u: &mut UnitState) -> Result<(), MapError> {
486 let expected: usize = u.tiles.iter().map(|t| t.rect.pixels()).sum();
487 if expected != u.window.pixels() {
488 return Err(MapError::Internal(format!(
489 "unit {:?} tiles cover {expected} of {} pixels",
490 u.dir,
491 u.window.pixels()
492 )));
493 }
494 for (p, f) in u.files.drain(..) {
495 let tmp = partial(&Self::data_path(&u.dir, p));
496 f.sync_all().map_err(|e| io_at(&tmp, e))?;
497 drop(f);
498 fs::rename(&tmp, Self::data_path(&u.dir, p)).map_err(|e| io_at(&tmp, e))?;
499 }
500 if !sync_dir(&u.dir)? {
501 self.dir_sync_unsupported += 1;
502 }
503 u.tiles.sort_by_key(|t| (t.tile_row, t.tile_col));
504 let mut doc = u.header.clone();
505 doc["status"] = "complete".into();
506 doc["raw_wse_valid"] = u.tiles.iter().map(|t| t.raw_wse_valid).sum::<u64>().into();
507 doc["wet"] = u.tiles.iter().map(|t| t.wet).sum::<u64>().into();
508 doc["data_bytes"] = u.bytes.into();
509 doc["tiles"] = serde_json::Value::Null; let names: Vec<&'static str> = self.products.iter().map(|p| p.name()).collect();
511 let map = doc
512 .as_object()
513 .ok_or_else(|| MapError::Internal("unit header is not an object".into()))?;
514 let path = u.dir.join("unit.json");
515 write_atomic(&path, |w| {
516 serde_json::to_writer_pretty(
517 w,
518 &UnitDoc {
519 doc: map,
520 tiles: &u.tiles,
521 names: &names,
522 },
523 )
524 .map_err(|e| io_at(&path, std::io::Error::other(e)))
525 })
526 }
527
528 pub fn abort_all(&mut self, e: &MapError) -> Vec<String> {
531 let units = std::mem::take(&mut self.units);
532 let mut problems = Vec::new();
533 for (_, u) in units {
534 problems.extend(Self::fail_unit(u, e, &self.products));
535 }
536 problems
537 }
538
539 pub fn open_units(&self) -> usize {
540 self.units.len()
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[test]
549 fn rect_intersection() {
550 let a = Rect {
551 col: 0,
552 row: 0,
553 width: 10,
554 height: 10,
555 };
556 let b = Rect {
557 col: 5,
558 row: 8,
559 width: 10,
560 height: 10,
561 };
562 assert_eq!(
563 a.intersect(&b),
564 Some(Rect {
565 col: 5,
566 row: 8,
567 width: 5,
568 height: 2
569 })
570 );
571 let c = Rect {
572 col: 10,
573 row: 0,
574 width: 1,
575 height: 1,
576 };
577 assert_eq!(a.intersect(&c), None);
578 }
579
580 #[test]
581 fn fnv_reference() {
582 assert_eq!(fnv1a64(b""), 0xcbf29ce484222325);
583 assert_eq!(fnv1a64(b"a"), 0xaf63dc4c8601ec8c);
584 }
585
586 fn scratch(name: &str) -> PathBuf {
587 let d =
588 std::env::temp_dir().join(format!("ras2fim-sink-unit-{}-{name}", std::process::id()));
589 let _ = fs::remove_dir_all(&d);
590 d
591 }
592
593 fn tile(profile: usize, rect: Rect) -> TileOut {
594 let n = rect.pixels();
595 TileOut {
596 profile,
597 tile: (0, 0),
598 rect,
599 raw_wse: vec![1.5; n],
600 wse: vec![1.5; n],
601 depth: vec![0.5; n],
602 ground: vec![1.0; n],
603 simplex_id: vec![3; n],
604 }
605 }
606
607 fn names(dir: &Path) -> Vec<String> {
608 let mut v: Vec<String> = fs::read_dir(dir)
609 .unwrap()
610 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
611 .collect();
612 v.sort();
613 v
614 }
615
616 const R: Rect = Rect {
617 col: 0,
618 row: 0,
619 width: 4,
620 height: 2,
621 };
622
623 #[test]
624 fn commit_renames_partials_and_streams_unit_json() {
625 let d = scratch("commit");
626 let mut s = DirSink::create(&d, &[Product::Wse, Product::Depth]).unwrap();
627 let h = serde_json::json!({"schema": "x", "profile": "p"});
628 s.begin_unit((0, 0), "p", "a", Some(R), &h, 1).unwrap();
629 let u = d.join("units/p/a");
630 assert_eq!(
631 names(&u),
632 ["depth.bin.partial", "unit.json", "wse.bin.partial"]
633 );
634 let marker: serde_json::Value =
635 serde_json::from_slice(&fs::read(u.join("unit.json")).unwrap()).unwrap();
636 assert_eq!(marker["status"], "in_progress");
637 s.write_tile(&tile(0, R), 1).unwrap();
638 s.end_unit((0, 0), &h, None).unwrap();
639 assert_eq!(names(&u), ["depth.bin", "unit.json", "wse.bin"]);
640 let doc: serde_json::Value =
641 serde_json::from_slice(&fs::read(u.join("unit.json")).unwrap()).unwrap();
642 assert_eq!(doc["status"], "complete");
643 assert_eq!(
644 doc["tiles"][0]["fnv1a64"]["wse"].as_str().unwrap().len(),
645 16
646 );
647 let mut r1 = h.clone();
649 r1["status"] = "complete".into();
650 r1["raw_wse_valid"] = 8.into();
651 r1["wet"] = 8.into();
652 r1["data_bytes"] = 128.into();
653 r1["tiles"] = doc["tiles"].clone();
654 assert_eq!(
655 fs::read(u.join("unit.json")).unwrap(),
656 serde_json::to_vec_pretty(&r1).unwrap()
657 );
658 assert_eq!(fs::read(u.join("wse.bin")).unwrap().len(), 64);
659 let _ = fs::remove_dir_all(&d);
660 }
661
662 #[test]
663 fn failed_and_aborted_units_leave_no_data() {
664 let d = scratch("abort");
665 let mut s = DirSink::create(&d, &[Product::Wse]).unwrap();
666 let h = serde_json::json!({"schema": "x"});
667 s.begin_unit((0, 0), "p", "a", Some(R), &h, 1).unwrap();
668 s.begin_unit((0, 1), "p", "b", Some(R), &h, 1).unwrap();
669 s.write_tile(&tile(0, R), 2).unwrap();
670 s.end_unit((0, 0), &h, Some(&MapError::Terrain("bad block".into())))
671 .unwrap();
672 assert!(s.abort_all(&MapError::Io("disk full".into())).is_empty());
673 assert_eq!(s.open_units(), 0);
674 for (aoi, msg) in [("a", "bad block"), ("b", "disk full")] {
675 let u = d.join("units/p").join(aoi);
676 assert_eq!(names(&u), ["unit.json"], "{aoi}");
677 let doc: serde_json::Value =
678 serde_json::from_slice(&fs::read(u.join("unit.json")).unwrap()).unwrap();
679 assert_eq!(doc["status"], "failed");
680 assert!(doc["error"]["message"].as_str().unwrap().contains(msg));
681 }
682 let _ = fs::remove_dir_all(&d);
683 }
684
685 #[test]
686 fn incomplete_coverage_fails_the_unit() {
687 let d = scratch("cover");
688 let mut s = DirSink::create(&d, &[Product::Wse]).unwrap();
689 let h = serde_json::json!({"schema": "x"});
690 s.begin_unit((0, 0), "p", "a", Some(R), &h, 1).unwrap();
691 let e = s.end_unit((0, 0), &h, None).unwrap_err();
692 assert_eq!(e.kind(), "internal");
693 let u = d.join("units/p/a");
694 assert_eq!(names(&u), ["unit.json"]);
695 let _ = fs::remove_dir_all(&d);
696 }
697
698 #[test]
699 fn write_json_is_atomic_replace() {
700 let d = scratch("json");
701 fs::create_dir_all(&d).unwrap();
702 let p = d.join("r.json");
703 write_json(&p, &serde_json::json!({"a": 1})).unwrap();
704 write_json(&p, &serde_json::json!({"a": 2})).unwrap();
705 assert_eq!(names(&d), ["r.json"]);
706 assert_eq!(
707 fs::read(&p).unwrap(),
708 serde_json::to_vec_pretty(&serde_json::json!({"a": 2})).unwrap()
709 );
710 let _ = fs::remove_dir_all(&d);
711 }
712}