1#![forbid(unsafe_code)]
7
8mod points;
9
10use std::collections::{BTreeMap, HashSet};
11use std::fs::File;
12use std::io::{Read, Seek, SeekFrom};
13use std::path::Path;
14
15use byteorder::{LittleEndian, ReadBytesExt};
16use copc_core::{
17 CopcInfo, Entry, EntryAvailability, Error, HierarchyPage, Result, VoxelKey,
18 HIERARCHY_ENTRY_BYTES,
19};
20use las::{Transform, Vector};
21use laz::LazVlr;
22
23pub use points::{BoundsSelection, CopcReader, LodSelection, PointIter, PointQuery};
24
25const LAS_HEADER_SIZE_14: u16 = 375;
26const VLR_HEADER_BYTES: u64 = 54;
27const EVLR_HEADER_BYTES: u64 = 60;
28const MAX_VLR_COUNT: u32 = 4_096;
29const MAX_EVLR_COUNT: u32 = 4_096;
30const MAX_HIERARCHY_PAGE_BYTES: u64 = 64 * 1024 * 1024;
31const MAX_HIERARCHY_TOTAL_BYTES: u64 = 256 * 1024 * 1024;
32
33#[derive(Debug, Clone)]
35pub struct CopcFile {
36 header: LasHeader,
37 copc_info: CopcInfo,
38 laszip_vlr: LazVlr,
39 root_hierarchy: HierarchyPage,
40 hierarchy: BTreeMap<VoxelKey, Entry>,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct LasHeader {
46 pub point_data_record_format: u8,
47 pub point_data_record_length: u16,
48 pub offset_to_point_data: u32,
49 pub number_of_vlrs: u32,
50 pub x_scale_factor: f64,
51 pub y_scale_factor: f64,
52 pub z_scale_factor: f64,
53 pub x_offset: f64,
54 pub y_offset: f64,
55 pub z_offset: f64,
56 pub min_x: f64,
57 pub max_x: f64,
58 pub min_y: f64,
59 pub max_y: f64,
60 pub min_z: f64,
61 pub max_z: f64,
62 pub offset_to_first_evlr: u64,
63 pub number_of_evlrs: u32,
64 pub number_of_points: u64,
65}
66
67#[derive(Debug, Clone)]
68struct Vlr {
69 user_id: String,
70 record_id: u16,
71 data: Vec<u8>,
72}
73
74#[derive(Debug, Clone, Copy)]
75struct EvlrRef {
76 user_id: [u8; 16],
77 record_id: u16,
78 data_offset: u64,
79}
80
81impl CopcFile {
82 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
83 let mut file = File::open(path.as_ref()).map_err(|e| Error::io("open COPC file", e))?;
84 Self::from_reader(&mut file)
85 }
86
87 pub fn from_reader<R: Read + Seek>(reader: &mut R) -> Result<Self> {
88 let file_len = reader_len(reader)?;
89 let header = read_las_header(reader, file_len)?;
90 let vlrs = read_vlrs(
91 reader,
92 header.number_of_vlrs,
93 file_len,
94 u64::from(header.offset_to_point_data),
95 )?;
96 let copc_info_vlr = vlrs
97 .iter()
98 .find(|vlr| vlr.user_id == "copc" && vlr.record_id == 1)
99 .ok_or_else(|| Error::InvalidData("missing COPC info VLR".into()))?;
100 let copc_info = CopcInfo::from_le_bytes(&copc_info_vlr.data)?;
101 let laszip_vlr = vlrs
102 .iter()
103 .find(|vlr| vlr.user_id == "laszip encoded" && vlr.record_id == 22204)
104 .map(|vlr| {
105 LazVlr::read_from(vlr.data.as_slice()).map_err(|e| Error::Las(e.to_string()))
106 })
107 .transpose()?
108 .ok_or_else(|| Error::InvalidData("missing LASzip VLR".into()))?;
109 let evlrs = read_evlr_refs(reader, &header, file_len)?;
110 let root_evlr = evlrs
111 .iter()
112 .find(|evlr| trim_nul(&evlr.user_id) == "copc" && evlr.record_id == 1000)
113 .copied()
114 .ok_or_else(|| Error::InvalidData("missing COPC hierarchy EVLR".into()))?;
115 if copc_info.root_hier_offset != root_evlr.data_offset {
116 return Err(Error::InvalidData(format!(
117 "COPC root hierarchy offset {} does not match EVLR data offset {}",
118 copc_info.root_hier_offset, root_evlr.data_offset
119 )));
120 }
121 let mut hierarchy_limits = HierarchyReadLimits::default();
122 let root_hierarchy = read_hierarchy_page_at(
123 reader,
124 copc_info.root_hier_offset,
125 copc_info.root_hier_size,
126 file_len,
127 &mut hierarchy_limits,
128 )?;
129 let mut hierarchy = BTreeMap::new();
130 let mut visited_pages = HashSet::new();
131 visited_pages.insert((copc_info.root_hier_offset, copc_info.root_hier_size));
132 insert_hierarchy_page(
133 reader,
134 &root_hierarchy,
135 &mut hierarchy,
136 &mut visited_pages,
137 file_len,
138 &mut hierarchy_limits,
139 )?;
140 Ok(Self {
141 header,
142 copc_info,
143 laszip_vlr,
144 root_hierarchy,
145 hierarchy,
146 })
147 }
148
149 pub fn header(&self) -> &LasHeader {
150 &self.header
151 }
152
153 pub fn copc_info(&self) -> &CopcInfo {
154 &self.copc_info
155 }
156
157 pub fn root_hierarchy(&self) -> &HierarchyPage {
158 &self.root_hierarchy
159 }
160
161 pub fn hierarchy_walk(&self) -> Vec<Entry> {
163 self.hierarchy.values().copied().collect()
164 }
165
166 pub fn hierarchy(&self) -> &BTreeMap<VoxelKey, Entry> {
168 &self.hierarchy
169 }
170
171 pub fn hierarchy_entries(&self) -> impl Iterator<Item = &Entry> {
172 self.hierarchy.values()
173 }
174
175 pub(crate) fn laszip_vlr(&self) -> &LazVlr {
176 &self.laszip_vlr
177 }
178
179 pub(crate) fn point_format(&self) -> Result<las::point::Format> {
180 let format_id = self.header.point_data_record_format & 0x7F;
181 let mut format =
182 las::point::Format::new(format_id).map_err(|e| Error::Las(e.to_string()))?;
183 let base_len = format.len();
184 if self.header.point_data_record_length < base_len {
185 return Err(Error::InvalidData(format!(
186 "point record length {} is smaller than point format {} base length {}",
187 self.header.point_data_record_length, format_id, base_len
188 )));
189 }
190 format.extra_bytes = self.header.point_data_record_length - base_len;
191 Ok(format)
192 }
193
194 pub(crate) fn transforms(&self) -> Vector<Transform> {
195 Vector {
196 x: Transform {
197 scale: self.header.x_scale_factor,
198 offset: self.header.x_offset,
199 },
200 y: Transform {
201 scale: self.header.y_scale_factor,
202 offset: self.header.y_offset,
203 },
204 z: Transform {
205 scale: self.header.z_scale_factor,
206 offset: self.header.z_offset,
207 },
208 }
209 }
210}
211
212impl LasHeader {
213 pub fn number_of_points(&self) -> u64 {
214 self.number_of_points
215 }
216}
217
218#[derive(Debug, Default)]
219struct HierarchyReadLimits {
220 total_bytes: u64,
221}
222
223impl HierarchyReadLimits {
224 fn add_page(&mut self, byte_size: u64) -> Result<()> {
225 if byte_size > MAX_HIERARCHY_PAGE_BYTES {
226 return Err(Error::InvalidData(format!(
227 "hierarchy page is {byte_size} bytes, max supported is {MAX_HIERARCHY_PAGE_BYTES}"
228 )));
229 }
230 self.total_bytes = self
231 .total_bytes
232 .checked_add(byte_size)
233 .ok_or_else(|| Error::InvalidData("hierarchy byte total overflow".into()))?;
234 if self.total_bytes > MAX_HIERARCHY_TOTAL_BYTES {
235 return Err(Error::InvalidData(format!(
236 "hierarchy pages total {} bytes, max supported is {}",
237 self.total_bytes, MAX_HIERARCHY_TOTAL_BYTES
238 )));
239 }
240 Ok(())
241 }
242}
243
244fn reader_len<R: Seek>(reader: &mut R) -> Result<u64> {
245 let current = reader
246 .stream_position()
247 .map_err(|e| Error::io("record reader position", e))?;
248 let len = reader
249 .seek(SeekFrom::End(0))
250 .map_err(|e| Error::io("seek end of COPC file", e))?;
251 reader
252 .seek(SeekFrom::Start(current))
253 .map_err(|e| Error::io("restore reader position", e))?;
254 Ok(len)
255}
256
257fn checked_range_end(offset: u64, byte_size: u64, label: &str) -> Result<u64> {
258 offset
259 .checked_add(byte_size)
260 .ok_or_else(|| Error::InvalidData(format!("{label} offset/size overflow")))
261}
262
263fn validate_range_in_file(offset: u64, byte_size: u64, file_len: u64, label: &str) -> Result<u64> {
264 let end = checked_range_end(offset, byte_size, label)?;
265 if end > file_len {
266 return Err(Error::InvalidData(format!(
267 "{label} range {offset}..{end} exceeds file length {file_len}"
268 )));
269 }
270 Ok(end)
271}
272
273fn read_hierarchy_page_at<R: Read + Seek>(
274 reader: &mut R,
275 offset: u64,
276 byte_size: u64,
277 file_len: u64,
278 limits: &mut HierarchyReadLimits,
279) -> Result<HierarchyPage> {
280 if byte_size == 0 {
281 return Err(Error::InvalidData("hierarchy page is empty".into()));
282 }
283 if byte_size % HIERARCHY_ENTRY_BYTES as u64 != 0 {
284 return Err(Error::InvalidData(format!(
285 "hierarchy page is {byte_size} bytes, not a multiple of {HIERARCHY_ENTRY_BYTES}"
286 )));
287 }
288 limits.add_page(byte_size)?;
289 validate_range_in_file(offset, byte_size, file_len, "hierarchy page")?;
290 let hierarchy_len = usize::try_from(byte_size)
291 .map_err(|_| Error::InvalidData("hierarchy page is too large".into()))?;
292 let mut hierarchy_bytes = vec![0u8; hierarchy_len];
293 reader
294 .seek(SeekFrom::Start(offset))
295 .map_err(|e| Error::io("seek hierarchy page", e))?;
296 reader
297 .read_exact(&mut hierarchy_bytes)
298 .map_err(|e| Error::io("read hierarchy page", e))?;
299 HierarchyPage::from_le_bytes(&hierarchy_bytes)
300}
301
302fn insert_hierarchy_page<R: Read + Seek>(
303 reader: &mut R,
304 page: &HierarchyPage,
305 hierarchy: &mut BTreeMap<VoxelKey, Entry>,
306 visited_pages: &mut HashSet<(u64, u64)>,
307 file_len: u64,
308 limits: &mut HierarchyReadLimits,
309) -> Result<()> {
310 for entry in page.entries().iter().copied() {
311 validate_hierarchy_entry(entry, file_len)?;
312 hierarchy.insert(entry.key, entry);
313 }
314 for entry in page.entries().iter().copied().filter(|e| e.is_child_page()) {
315 let byte_size = u64::try_from(entry.byte_size).expect("validated child page byte size");
316 if visited_pages.insert((entry.offset, byte_size)) {
317 let child_page =
318 read_hierarchy_page_at(reader, entry.offset, byte_size, file_len, limits)?;
319 insert_hierarchy_page(
320 reader,
321 &child_page,
322 hierarchy,
323 visited_pages,
324 file_len,
325 limits,
326 )?;
327 }
328 }
329 Ok(())
330}
331
332fn validate_hierarchy_entry(entry: Entry, file_len: u64) -> Result<()> {
333 match entry.availability()? {
334 EntryAvailability::Empty => Ok(()),
335 EntryAvailability::PointData { .. } => {
336 if entry.byte_size <= 0 {
337 return Err(Error::InvalidData(format!(
338 "point data entry {:?} has invalid byte size {}",
339 entry.key, entry.byte_size
340 )));
341 }
342 let byte_size = u64::try_from(entry.byte_size).map_err(|_| {
343 Error::InvalidData(format!(
344 "point data entry {:?} has negative byte size {}",
345 entry.key, entry.byte_size
346 ))
347 })?;
348 validate_range_in_file(entry.offset, byte_size, file_len, "point data entry")?;
349 Ok(())
350 }
351 EntryAvailability::ChildPage => {
352 if entry.byte_size <= 0 {
353 return Err(Error::InvalidData(format!(
354 "child hierarchy page {:?} has invalid byte size {}",
355 entry.key, entry.byte_size
356 )));
357 }
358 let byte_size = u64::try_from(entry.byte_size).map_err(|_| {
359 Error::InvalidData(format!(
360 "child hierarchy page {:?} has negative byte size {}",
361 entry.key, entry.byte_size
362 ))
363 })?;
364 validate_range_in_file(entry.offset, byte_size, file_len, "child hierarchy page")?;
365 Ok(())
366 }
367 }
368}
369
370fn read_las_header<R: Read + Seek>(reader: &mut R, file_len: u64) -> Result<LasHeader> {
371 if file_len < u64::from(LAS_HEADER_SIZE_14) {
372 return Err(Error::InvalidData(format!(
373 "file is {file_len} bytes; COPC requires at least {LAS_HEADER_SIZE_14}"
374 )));
375 }
376 reader
377 .seek(SeekFrom::Start(0))
378 .map_err(|e| Error::io("seek LAS header", e))?;
379 let mut signature = [0u8; 4];
380 reader
381 .read_exact(&mut signature)
382 .map_err(|e| Error::io("read LAS signature", e))?;
383 if &signature != b"LASF" {
384 return Err(Error::InvalidData("missing LASF signature".into()));
385 }
386 reader
387 .seek(SeekFrom::Start(94))
388 .map_err(|e| Error::io("seek LAS header size", e))?;
389 let header_size = reader
390 .read_u16::<LittleEndian>()
391 .map_err(|e| Error::io("read LAS header size", e))?;
392 if header_size < LAS_HEADER_SIZE_14 {
393 return Err(Error::Unsupported(format!(
394 "LAS header is {header_size} bytes; COPC requires LAS 1.4"
395 )));
396 }
397 if u64::from(header_size) > file_len {
398 return Err(Error::InvalidData(format!(
399 "LAS header size {header_size} exceeds file length {file_len}"
400 )));
401 }
402 let offset_to_point_data = reader
403 .read_u32::<LittleEndian>()
404 .map_err(|e| Error::io("read point data offset", e))?;
405 if u64::from(offset_to_point_data) < u64::from(header_size) {
406 return Err(Error::InvalidData(format!(
407 "point data offset {offset_to_point_data} is before LAS header size {header_size}"
408 )));
409 }
410 if u64::from(offset_to_point_data) > file_len {
411 return Err(Error::InvalidData(format!(
412 "point data offset {offset_to_point_data} exceeds file length {file_len}"
413 )));
414 }
415 let number_of_vlrs = reader
416 .read_u32::<LittleEndian>()
417 .map_err(|e| Error::io("read VLR count", e))?;
418 if number_of_vlrs > MAX_VLR_COUNT {
419 return Err(Error::InvalidData(format!(
420 "VLR count {number_of_vlrs} exceeds max supported {MAX_VLR_COUNT}"
421 )));
422 }
423 let point_data_record_format = reader
424 .read_u8()
425 .map_err(|e| Error::io("read point record format", e))?;
426 let point_data_record_length = reader
427 .read_u16::<LittleEndian>()
428 .map_err(|e| Error::io("read point record length", e))?;
429 reader
430 .seek(SeekFrom::Start(131))
431 .map_err(|e| Error::io("seek LAS transforms", e))?;
432 let x_scale_factor = reader
433 .read_f64::<LittleEndian>()
434 .map_err(|e| Error::io("read x scale factor", e))?;
435 let y_scale_factor = reader
436 .read_f64::<LittleEndian>()
437 .map_err(|e| Error::io("read y scale factor", e))?;
438 let z_scale_factor = reader
439 .read_f64::<LittleEndian>()
440 .map_err(|e| Error::io("read z scale factor", e))?;
441 let x_offset = reader
442 .read_f64::<LittleEndian>()
443 .map_err(|e| Error::io("read x offset", e))?;
444 let y_offset = reader
445 .read_f64::<LittleEndian>()
446 .map_err(|e| Error::io("read y offset", e))?;
447 let z_offset = reader
448 .read_f64::<LittleEndian>()
449 .map_err(|e| Error::io("read z offset", e))?;
450 let max_x = reader
451 .read_f64::<LittleEndian>()
452 .map_err(|e| Error::io("read max x", e))?;
453 let min_x = reader
454 .read_f64::<LittleEndian>()
455 .map_err(|e| Error::io("read min x", e))?;
456 let max_y = reader
457 .read_f64::<LittleEndian>()
458 .map_err(|e| Error::io("read max y", e))?;
459 let min_y = reader
460 .read_f64::<LittleEndian>()
461 .map_err(|e| Error::io("read min y", e))?;
462 let max_z = reader
463 .read_f64::<LittleEndian>()
464 .map_err(|e| Error::io("read max z", e))?;
465 let min_z = reader
466 .read_f64::<LittleEndian>()
467 .map_err(|e| Error::io("read min z", e))?;
468 reader
469 .seek(SeekFrom::Start(235))
470 .map_err(|e| Error::io("seek LAS 1.4 fields", e))?;
471 let offset_to_first_evlr = reader
472 .read_u64::<LittleEndian>()
473 .map_err(|e| Error::io("read first EVLR offset", e))?;
474 let number_of_evlrs = reader
475 .read_u32::<LittleEndian>()
476 .map_err(|e| Error::io("read EVLR count", e))?;
477 if number_of_evlrs > MAX_EVLR_COUNT {
478 return Err(Error::InvalidData(format!(
479 "EVLR count {number_of_evlrs} exceeds max supported {MAX_EVLR_COUNT}"
480 )));
481 }
482 if offset_to_first_evlr != 0 && offset_to_first_evlr > file_len {
483 return Err(Error::InvalidData(format!(
484 "first EVLR offset {offset_to_first_evlr} exceeds file length {file_len}"
485 )));
486 }
487 let number_of_points = reader
488 .read_u64::<LittleEndian>()
489 .map_err(|e| Error::io("read point count", e))?;
490 reader
491 .seek(SeekFrom::Start(u64::from(header_size)))
492 .map_err(|e| Error::io("seek after LAS header", e))?;
493 Ok(LasHeader {
494 point_data_record_format,
495 point_data_record_length,
496 offset_to_point_data,
497 number_of_vlrs,
498 x_scale_factor,
499 y_scale_factor,
500 z_scale_factor,
501 x_offset,
502 y_offset,
503 z_offset,
504 min_x,
505 max_x,
506 min_y,
507 max_y,
508 min_z,
509 max_z,
510 offset_to_first_evlr,
511 number_of_evlrs,
512 number_of_points,
513 })
514}
515
516fn read_vlrs<R: Read + Seek>(
517 reader: &mut R,
518 count: u32,
519 file_len: u64,
520 section_end: u64,
521) -> Result<Vec<Vlr>> {
522 if count > MAX_VLR_COUNT {
523 return Err(Error::InvalidData(format!(
524 "VLR count {count} exceeds max supported {MAX_VLR_COUNT}"
525 )));
526 }
527 if section_end > file_len {
528 return Err(Error::InvalidData(format!(
529 "VLR section end {section_end} exceeds file length {file_len}"
530 )));
531 }
532 let mut vlrs = Vec::new();
533 for index in 0..count {
534 let header_offset = reader
535 .stream_position()
536 .map_err(|e| Error::io("record VLR offset", e))?;
537 validate_range_in_file(header_offset, VLR_HEADER_BYTES, section_end, "VLR header")?;
538 let _reserved = reader
539 .read_u16::<LittleEndian>()
540 .map_err(|e| Error::io("read VLR reserved", e))?;
541 let mut user_id = [0u8; 16];
542 reader
543 .read_exact(&mut user_id)
544 .map_err(|e| Error::io("read VLR user id", e))?;
545 let record_id = reader
546 .read_u16::<LittleEndian>()
547 .map_err(|e| Error::io("read VLR record id", e))?;
548 let record_length = reader
549 .read_u16::<LittleEndian>()
550 .map_err(|e| Error::io("read VLR length", e))?;
551 let mut description = [0u8; 32];
552 reader
553 .read_exact(&mut description)
554 .map_err(|e| Error::io("read VLR description", e))?;
555 let data_offset = reader
556 .stream_position()
557 .map_err(|e| Error::io("record VLR data offset", e))?;
558 let data_end = validate_range_in_file(
559 data_offset,
560 u64::from(record_length),
561 section_end,
562 "VLR data",
563 )?;
564 let user_id_str = trim_nul(&user_id).to_string();
565 if should_store_vlr(&user_id_str, record_id) {
566 let mut data = vec![0u8; usize::from(record_length)];
567 reader
568 .read_exact(&mut data)
569 .map_err(|e| Error::io("read VLR data", e))?;
570 vlrs.push(Vlr {
571 user_id: user_id_str,
572 record_id,
573 data,
574 });
575 } else {
576 reader
577 .seek(SeekFrom::Start(data_end))
578 .map_err(|e| Error::io("skip VLR data", e))?;
579 }
580 let actual_next = reader
581 .stream_position()
582 .map_err(|e| Error::io("record next VLR offset", e))?;
583 if actual_next != data_end {
584 return Err(Error::InvalidData(format!(
585 "VLR {index} cursor at {actual_next}, expected {data_end}"
586 )));
587 }
588 }
589 Ok(vlrs)
590}
591
592fn should_store_vlr(user_id: &str, record_id: u16) -> bool {
593 (user_id == "copc" && record_id == 1) || (user_id == "laszip encoded" && record_id == 22204)
594}
595
596fn read_evlr_refs<R: Read + Seek>(
597 reader: &mut R,
598 header: &LasHeader,
599 file_len: u64,
600) -> Result<Vec<EvlrRef>> {
601 if header.offset_to_first_evlr == 0 || header.number_of_evlrs == 0 {
602 return Ok(Vec::new());
603 }
604 if header.number_of_evlrs > MAX_EVLR_COUNT {
605 return Err(Error::InvalidData(format!(
606 "EVLR count {} exceeds max supported {}",
607 header.number_of_evlrs, MAX_EVLR_COUNT
608 )));
609 }
610 validate_range_in_file(
611 header.offset_to_first_evlr,
612 EVLR_HEADER_BYTES,
613 file_len,
614 "first EVLR header",
615 )?;
616 reader
617 .seek(SeekFrom::Start(header.offset_to_first_evlr))
618 .map_err(|e| Error::io("seek EVLRs", e))?;
619 let mut evlrs = Vec::new();
620 for index in 0..header.number_of_evlrs {
621 let header_start = reader
622 .stream_position()
623 .map_err(|e| Error::io("record EVLR offset", e))?;
624 validate_range_in_file(header_start, EVLR_HEADER_BYTES, file_len, "EVLR header")?;
625 let _reserved = reader
626 .read_u16::<LittleEndian>()
627 .map_err(|e| Error::io("read EVLR reserved", e))?;
628 let mut user_id = [0u8; 16];
629 reader
630 .read_exact(&mut user_id)
631 .map_err(|e| Error::io("read EVLR user id", e))?;
632 let record_id = reader
633 .read_u16::<LittleEndian>()
634 .map_err(|e| Error::io("read EVLR record id", e))?;
635 let data_len = reader
636 .read_u64::<LittleEndian>()
637 .map_err(|e| Error::io("read EVLR length", e))?;
638 let mut description = [0u8; 32];
639 reader
640 .read_exact(&mut description)
641 .map_err(|e| Error::io("read EVLR description", e))?;
642 let data_offset = reader
643 .stream_position()
644 .map_err(|e| Error::io("record EVLR data offset", e))?;
645 evlrs.push(EvlrRef {
646 user_id,
647 record_id,
648 data_offset,
649 });
650 let expected_next = validate_range_in_file(data_offset, data_len, file_len, "EVLR data")?;
651 reader
652 .seek(SeekFrom::Start(expected_next))
653 .map_err(|e| Error::io("skip EVLR data", e))?;
654 let actual_next = reader
655 .stream_position()
656 .map_err(|e| Error::io("record next EVLR offset", e))?;
657 if actual_next != expected_next {
658 return Err(Error::InvalidData(format!(
659 "EVLR {index} cursor at {actual_next}, expected {expected_next}"
660 )));
661 }
662 }
663 Ok(evlrs)
664}
665
666fn trim_nul(bytes: &[u8]) -> &str {
667 let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
668 std::str::from_utf8(&bytes[..end]).unwrap_or("")
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 use byteorder::{LittleEndian, WriteBytesExt};
676 use copc_core::{EntryAvailability, HIERARCHY_ENTRY_BYTES};
677 use laz::LazVlrBuilder;
678 use std::io::{Cursor, Write};
679
680 #[test]
681 fn hierarchy_walk_loads_recursive_child_pages() {
682 let mut fixture = Cursor::new(copc_with_child_hierarchy_page());
683 let file = CopcFile::from_reader(&mut fixture).unwrap();
684 let child_key = VoxelKey::root().child(3);
685 let grandchild_key = child_key.child(5);
686
687 assert_eq!(file.root_hierarchy().entries().len(), 2);
688 assert!(file.root_hierarchy().entries()[1].is_child_page());
689
690 let hierarchy = file.hierarchy();
691 assert_eq!(hierarchy.len(), 3);
692 assert_eq!(
693 hierarchy
694 .get(&VoxelKey::root())
695 .unwrap()
696 .availability()
697 .unwrap(),
698 EntryAvailability::PointData { point_count: 5 }
699 );
700 assert_eq!(
701 hierarchy.get(&child_key).unwrap().availability().unwrap(),
702 EntryAvailability::PointData { point_count: 4 }
703 );
704 assert_eq!(
705 hierarchy
706 .get(&grandchild_key)
707 .unwrap()
708 .availability()
709 .unwrap(),
710 EntryAvailability::PointData { point_count: 3 }
711 );
712 assert!(!hierarchy.values().any(|entry| entry.is_child_page()));
713
714 let walk = file.hierarchy_walk();
715 assert_eq!(walk.len(), hierarchy.len());
716 assert_eq!(walk.iter().map(|entry| entry.point_count).sum::<i32>(), 12);
717 }
718
719 #[test]
720 fn rejects_excessive_vlr_count_before_allocation() {
721 let mut bytes = copc_with_child_hierarchy_page();
722 put_u32(&mut bytes, 100, MAX_VLR_COUNT + 1);
723
724 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
725
726 assert!(err.to_string().contains("VLR count"));
727 }
728
729 #[test]
730 fn rejects_excessive_evlr_count_before_allocation() {
731 let mut bytes = copc_with_child_hierarchy_page();
732 put_u32(&mut bytes, 243, MAX_EVLR_COUNT + 1);
733
734 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
735
736 assert!(err.to_string().contains("EVLR count"));
737 }
738
739 #[test]
740 fn rejects_oversized_root_hierarchy_page_before_allocation() {
741 let mut bytes = copc_with_child_hierarchy_page();
742 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
743 put_u64(
744 &mut bytes,
745 copc_info_data + 48,
746 MAX_HIERARCHY_PAGE_BYTES + HIERARCHY_ENTRY_BYTES as u64,
747 );
748
749 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
750
751 assert!(err.to_string().contains("hierarchy page"));
752 assert!(err.to_string().contains("max supported"));
753 }
754
755 #[test]
756 fn rejects_child_hierarchy_page_outside_file() {
757 let mut bytes = copc_with_child_hierarchy_page();
758 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
759 let root_hier_offset = read_u64(&bytes, copc_info_data + 40) as usize;
760 let child_entry_offset_field = root_hier_offset + HIERARCHY_ENTRY_BYTES + 16;
761 let outside_file = bytes.len() as u64 + 1;
762 put_u64(&mut bytes, child_entry_offset_field, outside_file);
763
764 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
765
766 assert!(err.to_string().contains("child hierarchy page"));
767 assert!(err.to_string().contains("exceeds file length"));
768 }
769
770 #[test]
771 fn rejects_header_offsets_outside_file_before_allocation() {
772 for (offset, value, expected) in [
773 (94, u64::from(u16::MAX), "LAS header size"),
774 (96, 1, "point data offset"),
775 (96, u64::MAX, "point data offset"),
776 (235, u64::MAX, "first EVLR offset"),
777 ] {
778 let mut bytes = copc_with_child_hierarchy_page();
779 put_int(&mut bytes, offset, value);
780
781 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
782
783 assert!(
784 err.to_string().contains(expected),
785 "expected {expected:?}, got {err}"
786 );
787 }
788 }
789
790 #[test]
791 fn rejects_vlr_and_evlr_lengths_outside_file_before_allocation() {
792 let mut bytes = copc_with_child_hierarchy_page();
793 let first_vlr_length_field = usize::from(LAS_HEADER_SIZE_14) + 20;
794 put_u16(&mut bytes, first_vlr_length_field, u16::MAX);
795
796 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
797
798 assert!(err.to_string().contains("VLR data"));
799 assert!(err.to_string().contains("exceeds file length"));
800
801 let mut bytes = copc_with_child_hierarchy_page();
802 let evlr_start = read_u64(&bytes, 235) as usize;
803 let evlr_length_field = evlr_start + 20;
804 put_u64(&mut bytes, evlr_length_field, u64::MAX);
805
806 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
807
808 assert!(err.to_string().contains("EVLR data"));
809 assert!(
810 err.to_string().contains("overflow") || err.to_string().contains("exceeds file length")
811 );
812 }
813
814 #[test]
815 fn rejects_malformed_root_hierarchy_sizes() {
816 for (root_hier_size, expected) in [
817 (0, "empty"),
818 (HIERARCHY_ENTRY_BYTES as u64 - 1, "not a multiple"),
819 ] {
820 let mut bytes = copc_with_child_hierarchy_page();
821 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
822 put_u64(&mut bytes, copc_info_data + 48, root_hier_size);
823
824 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
825
826 assert!(
827 err.to_string().contains(expected),
828 "expected {expected:?}, got {err}"
829 );
830 }
831 }
832
833 #[test]
834 fn rejects_invalid_hierarchy_entry_byte_sizes() {
835 let mut bytes = copc_with_child_hierarchy_page();
836 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
837 let root_hier_offset = read_u64(&bytes, copc_info_data + 40) as usize;
838 put_i32(&mut bytes, root_hier_offset + 24, 0);
839
840 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
841
842 assert!(err.to_string().contains("point data entry"));
843 assert!(err.to_string().contains("invalid byte size"));
844
845 let mut bytes = copc_with_child_hierarchy_page();
846 let root_hier_offset = read_u64(&bytes, copc_info_data + 40) as usize;
847 put_i32(&mut bytes, root_hier_offset + HIERARCHY_ENTRY_BYTES + 24, 0);
848
849 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
850
851 assert!(err.to_string().contains("child hierarchy page"));
852 assert!(err.to_string().contains("invalid byte size"));
853 }
854
855 #[test]
856 fn truncated_inputs_fail_without_panicking() {
857 let bytes = copc_with_child_hierarchy_page();
858 for len in [
859 0,
860 1,
861 4,
862 128,
863 usize::from(LAS_HEADER_SIZE_14) - 1,
864 usize::from(LAS_HEADER_SIZE_14),
865 bytes.len() / 2,
866 bytes.len() - 1,
867 ] {
868 let truncated = bytes[..len].to_vec();
869
870 let err = CopcFile::from_reader(&mut Cursor::new(truncated)).unwrap_err();
871
872 assert!(
873 !err.to_string().is_empty(),
874 "truncated input length {len} produced an empty error"
875 );
876 }
877 }
878
879 fn copc_with_child_hierarchy_page() -> Vec<u8> {
880 let mut laz_vlr_bytes = Vec::new();
881 LazVlrBuilder::default()
882 .with_point_format(6, 0)
883 .unwrap()
884 .with_variable_chunk_size()
885 .build()
886 .write_to(&mut laz_vlr_bytes)
887 .unwrap();
888
889 let offset_to_point_data = u32::from(LAS_HEADER_SIZE_14)
890 + (54 + copc_core::info::COPC_INFO_BYTES as u32)
891 + (54 + laz_vlr_bytes.len() as u32);
892 let root_point_offset = u64::from(offset_to_point_data);
893 let child_point_offset = root_point_offset + 100;
894 let grandchild_point_offset = child_point_offset + 200;
895 let evlr_start = grandchild_point_offset + 220;
896 let root_hier_offset = evlr_start + 60;
897 let root_hier_size = (2 * HIERARCHY_ENTRY_BYTES) as u64;
898 let child_page_offset = root_hier_offset + root_hier_size;
899
900 let child_key = VoxelKey::root().child(3);
901 let grandchild_key = child_key.child(5);
902 let child_page = HierarchyPage::new(vec![
903 Entry {
904 key: child_key,
905 offset: child_point_offset,
906 byte_size: 200,
907 point_count: 4,
908 },
909 Entry {
910 key: grandchild_key,
911 offset: grandchild_point_offset,
912 byte_size: 220,
913 point_count: 3,
914 },
915 ]);
916 let child_page_bytes = child_page.write_le_bytes().unwrap();
917 let root_page = HierarchyPage::new(vec![
918 Entry {
919 key: VoxelKey::root(),
920 offset: root_point_offset,
921 byte_size: 100,
922 point_count: 5,
923 },
924 Entry {
925 key: child_key,
926 offset: child_page_offset,
927 byte_size: child_page_bytes.len() as i32,
928 point_count: -1,
929 },
930 ]);
931 let root_page_bytes = root_page.write_le_bytes().unwrap();
932
933 let info = CopcInfo {
934 center: (0.0, 0.0, 0.0),
935 halfsize: 10.0,
936 spacing: 1.0,
937 root_hier_offset,
938 root_hier_size,
939 gpstime_min: 0.0,
940 gpstime_max: 0.0,
941 };
942
943 let mut out = Vec::new();
944 write_las_header(&mut out, offset_to_point_data, evlr_start, 12);
945 write_vlr(&mut out, "copc", 1, &info.write_le_bytes(), "COPC info");
946 write_vlr(
947 &mut out,
948 "laszip encoded",
949 22204,
950 &laz_vlr_bytes,
951 "http://laszip.org",
952 );
953 assert_eq!(out.len(), offset_to_point_data as usize);
954 out.resize(evlr_start as usize, 0);
955
956 write_evlr_header(
957 &mut out,
958 "copc",
959 1000,
960 root_page_bytes.len() as u64,
961 "COPC hierarchy",
962 );
963 assert_eq!(out.len() as u64, root_hier_offset);
964 out.extend_from_slice(&root_page_bytes);
965 assert_eq!(out.len() as u64, child_page_offset);
966 out.extend_from_slice(&child_page_bytes);
967 out
968 }
969
970 fn write_las_header(
971 out: &mut Vec<u8>,
972 offset_to_point_data: u32,
973 evlr_start: u64,
974 point_count: u64,
975 ) {
976 out.resize(usize::from(LAS_HEADER_SIZE_14), 0);
977 out[0..4].copy_from_slice(b"LASF");
978 out[24] = 1;
979 out[25] = 4;
980 put_u16(out, 94, LAS_HEADER_SIZE_14);
981 put_u32(out, 96, offset_to_point_data);
982 put_u32(out, 100, 2);
983 out[104] = 6 | 0x80;
984 put_u16(out, 105, 30);
985 put_f64(out, 131, 0.001);
986 put_f64(out, 139, 0.001);
987 put_f64(out, 147, 0.001);
988 put_f64(out, 155, 0.0);
989 put_f64(out, 163, 0.0);
990 put_f64(out, 171, 0.0);
991 put_f64(out, 179, 10.0);
992 put_f64(out, 187, -10.0);
993 put_f64(out, 195, 10.0);
994 put_f64(out, 203, -10.0);
995 put_f64(out, 211, 10.0);
996 put_f64(out, 219, -10.0);
997 put_u64(out, 235, evlr_start);
998 put_u32(out, 243, 1);
999 put_u64(out, 247, point_count);
1000 }
1001
1002 fn write_vlr(out: &mut Vec<u8>, user_id: &str, record_id: u16, data: &[u8], desc: &str) {
1003 out.write_u16::<LittleEndian>(0).unwrap();
1004 out.write_all(&padded(user_id.as_bytes(), 16)).unwrap();
1005 out.write_u16::<LittleEndian>(record_id).unwrap();
1006 out.write_u16::<LittleEndian>(data.len() as u16).unwrap();
1007 out.write_all(&padded(desc.as_bytes(), 32)).unwrap();
1008 out.write_all(data).unwrap();
1009 }
1010
1011 fn write_evlr_header(
1012 out: &mut Vec<u8>,
1013 user_id: &str,
1014 record_id: u16,
1015 data_len: u64,
1016 desc: &str,
1017 ) {
1018 out.write_u16::<LittleEndian>(0).unwrap();
1019 out.write_all(&padded(user_id.as_bytes(), 16)).unwrap();
1020 out.write_u16::<LittleEndian>(record_id).unwrap();
1021 out.write_u64::<LittleEndian>(data_len).unwrap();
1022 out.write_all(&padded(desc.as_bytes(), 32)).unwrap();
1023 }
1024
1025 fn padded(bytes: &[u8], len: usize) -> Vec<u8> {
1026 let mut out = vec![0u8; len];
1027 let count = bytes.len().min(len);
1028 out[..count].copy_from_slice(&bytes[..count]);
1029 out
1030 }
1031
1032 fn put_u16(out: &mut [u8], offset: usize, value: u16) {
1033 out[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
1034 }
1035
1036 fn put_u32(out: &mut [u8], offset: usize, value: u32) {
1037 out[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1038 }
1039
1040 fn put_u64(out: &mut [u8], offset: usize, value: u64) {
1041 out[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1042 }
1043
1044 fn put_i32(out: &mut [u8], offset: usize, value: i32) {
1045 out[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1046 }
1047
1048 fn put_int(out: &mut [u8], offset: usize, value: u64) {
1049 match offset {
1050 94 => put_u16(out, offset, value as u16),
1051 96 => put_u32(out, offset, value as u32),
1052 235 => put_u64(out, offset, value),
1053 _ => unreachable!("unexpected integer offset"),
1054 }
1055 }
1056
1057 fn read_u64(bytes: &[u8], offset: usize) -> u64 {
1058 u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap())
1059 }
1060
1061 fn put_f64(out: &mut [u8], offset: usize, value: f64) {
1062 out[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1063 }
1064}