1#![forbid(unsafe_code)]
7
8mod points;
9mod range_read;
10mod ranged;
11
12use std::collections::{BTreeMap, HashSet};
13use std::fs::File;
14use std::io::{Read, Seek, SeekFrom};
15use std::path::Path;
16
17use byteorder::{LittleEndian, ReadBytesExt};
18use copc_core::{
19 CopcInfo, Entry, EntryAvailability, Error, HierarchyPage, Result, VoxelKey,
20 HIERARCHY_ENTRY_BYTES, MAX_EVLR_COUNT, MAX_VLR_COUNT,
21};
22use las::{Transform, Vector};
23use laz::LazVlr;
24
25pub use copc_core::{
26 ColumnData, ColumnSelection, ColumnSpec, ColumnView, LasColumnBatch, LasDimension, ScalarType,
27};
28pub use points::{BoundsSelection, CopcReader, LodSelection, PointIter, PointQuery};
29#[cfg(feature = "http")]
30pub use range_read::HttpRangeReader;
31pub use range_read::RangeRead;
32pub use ranged::CopcRangeReader;
33
34const LAS_HEADER_SIZE_14: u16 = 375;
35const VLR_HEADER_BYTES: u64 = 54;
36const EVLR_HEADER_BYTES: u64 = 60;
37const MAX_HIERARCHY_PAGE_BYTES: u64 = 64 * 1024 * 1024;
38const MAX_HIERARCHY_TOTAL_BYTES: u64 = 256 * 1024 * 1024;
39const MAX_POINT_CHUNK_BYTES: u64 = 512 * 1024 * 1024;
40
41#[derive(Debug, Clone)]
43pub struct CopcFile {
44 header: LasHeader,
45 copc_info: CopcInfo,
46 laszip_vlr: LazVlr,
47 root_hierarchy: HierarchyPage,
48 hierarchy: BTreeMap<VoxelKey, Entry>,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct LasHeader {
54 pub point_data_record_format: u8,
55 pub point_data_record_length: u16,
56 pub offset_to_point_data: u32,
57 pub number_of_vlrs: u32,
58 pub x_scale_factor: f64,
59 pub y_scale_factor: f64,
60 pub z_scale_factor: f64,
61 pub x_offset: f64,
62 pub y_offset: f64,
63 pub z_offset: f64,
64 pub min_x: f64,
65 pub max_x: f64,
66 pub min_y: f64,
67 pub max_y: f64,
68 pub min_z: f64,
69 pub max_z: f64,
70 pub offset_to_first_evlr: u64,
71 pub number_of_evlrs: u32,
72 pub number_of_points: u64,
73}
74
75#[derive(Debug, Clone)]
76struct Vlr {
77 index: u32,
78 user_id: String,
79 record_id: u16,
80 data: Vec<u8>,
81}
82
83#[derive(Debug, Clone, Copy)]
84struct EvlrRef {
85 user_id: [u8; 16],
86 record_id: u16,
87 data_offset: u64,
88 data_len: u64,
89}
90
91#[derive(Debug, Clone, Copy)]
92pub(crate) struct CopcDataRanges {
93 point_data_start: u64,
94 point_data_end: u64,
95 hierarchy_start: u64,
96 hierarchy_end: u64,
97}
98
99impl CopcFile {
100 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
101 let mut file = File::open(path.as_ref()).map_err(|e| Error::io("open COPC file", e))?;
102 Self::from_reader(&mut file)
103 }
104
105 pub fn from_reader<R: Read + Seek>(reader: &mut R) -> Result<Self> {
106 let file_len = reader_len(reader)?;
107 let header = read_las_header(reader, file_len)?;
108 let vlrs = read_vlrs(
109 reader,
110 header.number_of_vlrs,
111 file_len,
112 u64::from(header.offset_to_point_data),
113 )?;
114 let (copc_info, laszip_vlr) = extract_required_vlrs(&vlrs)?;
115 validate_hierarchy_page_byte_size(copc_info.root_hier_size)?;
116 let evlrs = read_evlr_refs(reader, &header, file_len)?;
117 let root_evlr = evlrs
118 .iter()
119 .find(|evlr| trim_nul(&evlr.user_id) == "copc" && evlr.record_id == 1000)
120 .copied()
121 .ok_or_else(|| Error::InvalidData("missing COPC hierarchy EVLR".into()))?;
122 if copc_info.root_hier_offset != root_evlr.data_offset {
123 return Err(Error::InvalidData(format!(
124 "COPC root hierarchy offset {} does not match EVLR data offset {}",
125 copc_info.root_hier_offset, root_evlr.data_offset
126 )));
127 }
128 if copc_info.root_hier_size > root_evlr.data_len {
129 return Err(Error::InvalidData(format!(
130 "COPC root hierarchy size {} exceeds hierarchy EVLR body size {}",
131 copc_info.root_hier_size, root_evlr.data_len
132 )));
133 }
134 let data_ranges = CopcDataRanges::new(&header, root_evlr.data_offset, root_evlr.data_len)?;
135 let mut hierarchy_limits = HierarchyReadLimits::default();
136 let root_hierarchy = read_hierarchy_page_at(
137 reader,
138 copc_info.root_hier_offset,
139 copc_info.root_hier_size,
140 file_len,
141 &mut hierarchy_limits,
142 )?;
143 let mut hierarchy = BTreeMap::new();
144 let mut visited_pages = HashSet::new();
145 visited_pages.insert((copc_info.root_hier_offset, copc_info.root_hier_size));
146 insert_hierarchy_pages(
147 reader,
148 &root_hierarchy,
149 &mut hierarchy,
150 &mut visited_pages,
151 file_len,
152 &mut hierarchy_limits,
153 &data_ranges,
154 )?;
155 validate_point_chunk_ranges(&hierarchy)?;
156 validate_hierarchy_point_count(&hierarchy, header.number_of_points)?;
157 Ok(Self {
158 header,
159 copc_info,
160 laszip_vlr,
161 root_hierarchy,
162 hierarchy,
163 })
164 }
165
166 pub fn header(&self) -> &LasHeader {
167 &self.header
168 }
169
170 pub fn copc_info(&self) -> &CopcInfo {
171 &self.copc_info
172 }
173
174 pub fn root_hierarchy(&self) -> &HierarchyPage {
175 &self.root_hierarchy
176 }
177
178 pub fn hierarchy_walk(&self) -> Vec<Entry> {
180 self.hierarchy.values().copied().collect()
181 }
182
183 pub fn hierarchy(&self) -> &BTreeMap<VoxelKey, Entry> {
185 &self.hierarchy
186 }
187
188 pub fn hierarchy_entries(&self) -> impl Iterator<Item = &Entry> {
189 self.hierarchy.values()
190 }
191
192 pub(crate) fn laszip_vlr(&self) -> &LazVlr {
193 &self.laszip_vlr
194 }
195
196 pub(crate) fn point_format(&self) -> Result<las::point::Format> {
197 point_format_for(&self.header)
198 }
199
200 pub(crate) fn transforms(&self) -> Vector<Transform> {
201 transforms_for(&self.header)
202 }
203}
204
205impl CopcDataRanges {
206 pub(crate) fn new(
207 header: &LasHeader,
208 hierarchy_start: u64,
209 hierarchy_len: u64,
210 ) -> Result<Self> {
211 let point_data_start = u64::from(header.offset_to_point_data);
212 let point_data_end = header.offset_to_first_evlr;
213 if point_data_end < point_data_start {
214 return Err(Error::InvalidData(format!(
215 "first EVLR offset {point_data_end} precedes point data offset {point_data_start}"
216 )));
217 }
218 let hierarchy_end =
219 checked_range_end(hierarchy_start, hierarchy_len, "hierarchy EVLR body")?;
220 Ok(Self {
221 point_data_start,
222 point_data_end,
223 hierarchy_start,
224 hierarchy_end,
225 })
226 }
227}
228
229pub(crate) fn point_format_for(header: &LasHeader) -> Result<las::point::Format> {
230 let format_id = header.point_data_record_format & 0x3F;
231 if !matches!(format_id, 6..=8) {
232 return Err(Error::Unsupported(format!(
233 "COPC requires LAS point format 6, 7, or 8; found {format_id}"
234 )));
235 }
236 let mut format = las::point::Format::new(format_id).map_err(|e| Error::Las(e.to_string()))?;
237 let base_len = format.len();
238 if header.point_data_record_length < base_len {
239 return Err(Error::InvalidData(format!(
240 "point record length {} is smaller than point format {} base length {}",
241 header.point_data_record_length, format_id, base_len
242 )));
243 }
244 format.extra_bytes = header.point_data_record_length - base_len;
245 Ok(format)
246}
247
248pub(crate) fn transforms_for(header: &LasHeader) -> Vector<Transform> {
249 Vector {
250 x: Transform {
251 scale: header.x_scale_factor,
252 offset: header.x_offset,
253 },
254 y: Transform {
255 scale: header.y_scale_factor,
256 offset: header.y_offset,
257 },
258 z: Transform {
259 scale: header.z_scale_factor,
260 offset: header.z_offset,
261 },
262 }
263}
264
265impl LasHeader {
266 pub fn number_of_points(&self) -> u64 {
267 self.number_of_points
268 }
269}
270
271#[derive(Debug, Default)]
272pub(crate) struct HierarchyReadLimits {
273 total_bytes: u64,
274}
275
276impl HierarchyReadLimits {
277 pub(crate) fn add_page(&mut self, byte_size: u64) -> Result<()> {
278 validate_hierarchy_page_byte_size(byte_size)?;
279 self.total_bytes = self
280 .total_bytes
281 .checked_add(byte_size)
282 .ok_or_else(|| Error::InvalidData("hierarchy byte total overflow".into()))?;
283 if self.total_bytes > MAX_HIERARCHY_TOTAL_BYTES {
284 return Err(Error::InvalidData(format!(
285 "hierarchy pages total {} bytes, max supported is {}",
286 self.total_bytes, MAX_HIERARCHY_TOTAL_BYTES
287 )));
288 }
289 Ok(())
290 }
291}
292
293pub(crate) fn validate_hierarchy_page_byte_size(byte_size: u64) -> Result<()> {
294 if byte_size > MAX_HIERARCHY_PAGE_BYTES {
295 return Err(Error::InvalidData(format!(
296 "hierarchy page is {byte_size} bytes, max supported is {MAX_HIERARCHY_PAGE_BYTES}"
297 )));
298 }
299 Ok(())
300}
301
302pub(crate) fn extract_required_vlrs(vlrs: &[Vlr]) -> Result<(CopcInfo, LazVlr)> {
304 let mut copc_info_vlrs = vlrs
305 .iter()
306 .filter(|vlr| vlr.user_id == "copc" && vlr.record_id == 1);
307 let copc_info_vlr = copc_info_vlrs
308 .next()
309 .ok_or_else(|| Error::InvalidData("missing COPC info VLR".into()))?;
310 if copc_info_vlrs.next().is_some() {
311 return Err(Error::InvalidData("duplicate COPC info VLR".into()));
312 }
313 if copc_info_vlr.index != 0 {
314 return Err(Error::InvalidData(
315 "COPC info VLR must be the first VLR".into(),
316 ));
317 }
318 let copc_info = CopcInfo::from_le_bytes(&copc_info_vlr.data)?;
319 let mut laszip_vlrs = vlrs
320 .iter()
321 .filter(|vlr| vlr.user_id == "laszip encoded" && vlr.record_id == 22204);
322 let laszip_vlr = laszip_vlrs
323 .next()
324 .ok_or_else(|| Error::InvalidData("missing LASzip VLR".into()))?;
325 if laszip_vlrs.next().is_some() {
326 return Err(Error::InvalidData("duplicate LASzip VLR".into()));
327 }
328 let laszip_vlr =
329 LazVlr::read_from(laszip_vlr.data.as_slice()).map_err(|e| Error::Las(e.to_string()))?;
330 Ok((copc_info, laszip_vlr))
331}
332
333fn reader_len<R: Seek>(reader: &mut R) -> Result<u64> {
334 let current = reader
335 .stream_position()
336 .map_err(|e| Error::io("record reader position", e))?;
337 let len = reader
338 .seek(SeekFrom::End(0))
339 .map_err(|e| Error::io("seek end of COPC file", e))?;
340 reader
341 .seek(SeekFrom::Start(current))
342 .map_err(|e| Error::io("restore reader position", e))?;
343 Ok(len)
344}
345
346fn checked_range_end(offset: u64, byte_size: u64, label: &str) -> Result<u64> {
347 offset
348 .checked_add(byte_size)
349 .ok_or_else(|| Error::InvalidData(format!("{label} offset/size overflow")))
350}
351
352fn validate_range_in_file(offset: u64, byte_size: u64, file_len: u64, label: &str) -> Result<u64> {
353 let end = checked_range_end(offset, byte_size, label)?;
354 if end > file_len {
355 return Err(Error::InvalidData(format!(
356 "{label} range {offset}..{end} exceeds file length {file_len}"
357 )));
358 }
359 Ok(end)
360}
361
362fn read_hierarchy_page_at<R: Read + Seek>(
363 reader: &mut R,
364 offset: u64,
365 byte_size: u64,
366 file_len: u64,
367 limits: &mut HierarchyReadLimits,
368) -> Result<HierarchyPage> {
369 if byte_size == 0 {
370 return Err(Error::InvalidData("hierarchy page is empty".into()));
371 }
372 if !byte_size.is_multiple_of(HIERARCHY_ENTRY_BYTES as u64) {
373 return Err(Error::InvalidData(format!(
374 "hierarchy page is {byte_size} bytes, not a multiple of {HIERARCHY_ENTRY_BYTES}"
375 )));
376 }
377 limits.add_page(byte_size)?;
378 validate_range_in_file(offset, byte_size, file_len, "hierarchy page")?;
379 let hierarchy_len = usize::try_from(byte_size)
380 .map_err(|_| Error::InvalidData("hierarchy page is too large".into()))?;
381 let mut hierarchy_bytes = vec![0u8; hierarchy_len];
382 reader
383 .seek(SeekFrom::Start(offset))
384 .map_err(|e| Error::io("seek hierarchy page", e))?;
385 reader
386 .read_exact(&mut hierarchy_bytes)
387 .map_err(|e| Error::io("read hierarchy page", e))?;
388 HierarchyPage::from_le_bytes(&hierarchy_bytes)
389}
390
391fn insert_hierarchy_pages<R: Read + Seek>(
395 reader: &mut R,
396 root_page: &HierarchyPage,
397 hierarchy: &mut BTreeMap<VoxelKey, Entry>,
398 visited_pages: &mut HashSet<(u64, u64)>,
399 file_len: u64,
400 limits: &mut HierarchyReadLimits,
401 data_ranges: &CopcDataRanges,
402) -> Result<()> {
403 let mut pending = Vec::new();
404 queue_hierarchy_page(
405 root_page,
406 hierarchy,
407 visited_pages,
408 &mut pending,
409 file_len,
410 data_ranges,
411 )?;
412 while let Some((offset, byte_size)) = pending.pop() {
413 let page = read_hierarchy_page_at(reader, offset, byte_size, file_len, limits)?;
414 queue_hierarchy_page(
415 &page,
416 hierarchy,
417 visited_pages,
418 &mut pending,
419 file_len,
420 data_ranges,
421 )?;
422 }
423 Ok(())
424}
425
426fn queue_hierarchy_page(
427 page: &HierarchyPage,
428 hierarchy: &mut BTreeMap<VoxelKey, Entry>,
429 visited_pages: &mut HashSet<(u64, u64)>,
430 pending: &mut Vec<(u64, u64)>,
431 file_len: u64,
432 data_ranges: &CopcDataRanges,
433) -> Result<()> {
434 for entry in page.entries().iter().copied() {
435 validate_hierarchy_entry(entry, file_len, data_ranges)?;
436 match hierarchy.get(&entry.key).copied() {
437 None => {
438 hierarchy.insert(entry.key, entry);
439 }
440 Some(previous) if previous.is_child_page() && !entry.is_child_page() => {
441 hierarchy.insert(entry.key, entry);
442 }
443 Some(previous) => {
444 return Err(Error::InvalidData(format!(
445 "duplicate hierarchy key {:?}: previous {previous:?}, new {entry:?}",
446 entry.key
447 )));
448 }
449 }
450 }
451 for entry in page.entries().iter().copied().filter(|e| e.is_child_page()) {
452 let byte_size = u64::try_from(entry.byte_size).map_err(|_| {
453 Error::InvalidData(format!(
454 "child hierarchy page {:?} has negative byte size {}",
455 entry.key, entry.byte_size
456 ))
457 })?;
458 if visited_pages.insert((entry.offset, byte_size)) {
459 pending.push((entry.offset, byte_size));
460 }
461 }
462 Ok(())
463}
464
465fn validate_hierarchy_point_count(
468 hierarchy: &BTreeMap<VoxelKey, Entry>,
469 number_of_points: u64,
470) -> Result<()> {
471 let mut total: u64 = 0;
472 for entry in hierarchy.values() {
473 if let EntryAvailability::PointData { point_count } = entry.availability()? {
474 total = total
475 .checked_add(u64::from(point_count))
476 .ok_or_else(|| Error::InvalidData("hierarchy point total overflows u64".into()))?;
477 }
478 }
479 if total != number_of_points {
480 return Err(Error::InvalidData(format!(
481 "hierarchy point total {total} does not match LAS header point count {number_of_points}"
482 )));
483 }
484 Ok(())
485}
486
487fn validate_point_chunk_ranges(hierarchy: &BTreeMap<VoxelKey, Entry>) -> Result<()> {
488 let mut ranges = BTreeMap::new();
489 for entry in hierarchy
490 .values()
491 .copied()
492 .filter(|entry| entry.has_point_data())
493 {
494 insert_point_chunk_range(&mut ranges, entry)?;
495 }
496 Ok(())
497}
498
499pub(crate) fn insert_point_chunk_range(
500 ranges: &mut BTreeMap<u64, (u64, VoxelKey)>,
501 entry: Entry,
502) -> Result<()> {
503 let byte_size = u64::try_from(entry.byte_size).map_err(|_| {
504 Error::InvalidData(format!(
505 "point data entry {:?} has negative byte size {}",
506 entry.key, entry.byte_size
507 ))
508 })?;
509 let end = checked_range_end(entry.offset, byte_size, "point data entry")?;
510 if let Some((_, (previous_end, previous_key))) = ranges.range(..=entry.offset).next_back() {
511 if *previous_end > entry.offset {
512 return Err(Error::InvalidData(format!(
513 "point chunks {previous_key:?} and {:?} overlap",
514 entry.key
515 )));
516 }
517 }
518 if let Some((next_start, (_, next_key))) = ranges.range(entry.offset..).next() {
519 if *next_start < end {
520 return Err(Error::InvalidData(format!(
521 "point chunks {:?} and {next_key:?} overlap",
522 entry.key
523 )));
524 }
525 }
526 ranges.insert(entry.offset, (end, entry.key));
527 Ok(())
528}
529
530pub(crate) fn validate_hierarchy_entry(
531 entry: Entry,
532 file_len: u64,
533 data_ranges: &CopcDataRanges,
534) -> Result<()> {
535 entry.validate()?;
536 match entry.availability()? {
537 EntryAvailability::Empty => Ok(()),
538 EntryAvailability::PointData { .. } => {
539 let byte_size = u64::try_from(entry.byte_size).map_err(|_| {
540 Error::InvalidData(format!(
541 "point data entry {:?} has negative byte size {}",
542 entry.key, entry.byte_size
543 ))
544 })?;
545 if byte_size > MAX_POINT_CHUNK_BYTES {
546 return Err(Error::InvalidData(format!(
547 "point data entry {:?} is {byte_size} bytes, max supported is {MAX_POINT_CHUNK_BYTES}",
548 entry.key
549 )));
550 }
551 let end =
552 validate_range_in_file(entry.offset, byte_size, file_len, "point data entry")?;
553 if entry.offset < data_ranges.point_data_start || end > data_ranges.point_data_end {
554 return Err(Error::InvalidData(format!(
555 "point data entry {:?} range {}..{end} is outside LAS point-data section {}..{}",
556 entry.key,
557 entry.offset,
558 data_ranges.point_data_start,
559 data_ranges.point_data_end
560 )));
561 }
562 Ok(())
563 }
564 EntryAvailability::ChildPage => {
565 let byte_size = u64::try_from(entry.byte_size).map_err(|_| {
566 Error::InvalidData(format!(
567 "child hierarchy page {:?} has negative byte size {}",
568 entry.key, entry.byte_size
569 ))
570 })?;
571 let end =
572 validate_range_in_file(entry.offset, byte_size, file_len, "child hierarchy page")?;
573 if entry.offset < data_ranges.hierarchy_start || end > data_ranges.hierarchy_end {
574 return Err(Error::InvalidData(format!(
575 "child hierarchy page {:?} range {}..{end} is outside hierarchy EVLR body {}..{}",
576 entry.key,
577 entry.offset,
578 data_ranges.hierarchy_start,
579 data_ranges.hierarchy_end
580 )));
581 }
582 Ok(())
583 }
584 }
585}
586
587fn read_las_header<R: Read + Seek>(reader: &mut R, file_len: u64) -> Result<LasHeader> {
588 if file_len < u64::from(LAS_HEADER_SIZE_14) {
589 return Err(Error::InvalidData(format!(
590 "file is {file_len} bytes; COPC requires at least {LAS_HEADER_SIZE_14}"
591 )));
592 }
593 reader
594 .seek(SeekFrom::Start(0))
595 .map_err(|e| Error::io("seek LAS header", e))?;
596 let mut signature = [0u8; 4];
597 reader
598 .read_exact(&mut signature)
599 .map_err(|e| Error::io("read LAS signature", e))?;
600 if &signature != b"LASF" {
601 return Err(Error::InvalidData("missing LASF signature".into()));
602 }
603 reader
604 .seek(SeekFrom::Start(6))
605 .map_err(|e| Error::io("seek LAS global encoding", e))?;
606 let global_encoding = reader
607 .read_u16::<LittleEndian>()
608 .map_err(|e| Error::io("read LAS global encoding", e))?;
609 if global_encoding & !0x1F != 0 {
610 return Err(Error::InvalidData(format!(
611 "LAS global encoding has reserved bits set: {global_encoding:#06x}"
612 )));
613 }
614 if global_encoding & 0x10 == 0 {
615 return Err(Error::InvalidData(
616 "COPC point formats require the LAS WKT global-encoding bit".into(),
617 ));
618 }
619 reader
620 .seek(SeekFrom::Start(24))
621 .map_err(|e| Error::io("seek LAS version", e))?;
622 let version_major = reader
623 .read_u8()
624 .map_err(|e| Error::io("read LAS version major", e))?;
625 let version_minor = reader
626 .read_u8()
627 .map_err(|e| Error::io("read LAS version minor", e))?;
628 if (version_major, version_minor) != (1, 4) {
629 return Err(Error::Unsupported(format!(
630 "COPC requires LAS 1.4; found LAS {version_major}.{version_minor}"
631 )));
632 }
633 reader
634 .seek(SeekFrom::Start(94))
635 .map_err(|e| Error::io("seek LAS header size", e))?;
636 let header_size = reader
637 .read_u16::<LittleEndian>()
638 .map_err(|e| Error::io("read LAS header size", e))?;
639 if header_size != LAS_HEADER_SIZE_14 {
640 return Err(Error::InvalidData(format!(
641 "LAS header size {header_size} is invalid; COPC requires exactly {LAS_HEADER_SIZE_14} bytes"
642 )));
643 }
644 let offset_to_point_data = reader
645 .read_u32::<LittleEndian>()
646 .map_err(|e| Error::io("read point data offset", e))?;
647 if u64::from(offset_to_point_data) < u64::from(header_size) {
648 return Err(Error::InvalidData(format!(
649 "point data offset {offset_to_point_data} is before LAS header size {header_size}"
650 )));
651 }
652 if u64::from(offset_to_point_data) > file_len {
653 return Err(Error::InvalidData(format!(
654 "point data offset {offset_to_point_data} exceeds file length {file_len}"
655 )));
656 }
657 let number_of_vlrs = reader
658 .read_u32::<LittleEndian>()
659 .map_err(|e| Error::io("read VLR count", e))?;
660 if number_of_vlrs > MAX_VLR_COUNT {
661 return Err(Error::InvalidData(format!(
662 "VLR count {number_of_vlrs} exceeds max supported {MAX_VLR_COUNT}"
663 )));
664 }
665 let point_data_record_format = reader
666 .read_u8()
667 .map_err(|e| Error::io("read point record format", e))?;
668 if point_data_record_format & 0xC0 == 0 {
669 return Err(Error::InvalidData(
670 "COPC point data must be LAZ-compressed".into(),
671 ));
672 }
673 let point_format_id = point_data_record_format & 0x3F;
674 if !matches!(point_format_id, 6..=8) {
675 return Err(Error::Unsupported(format!(
676 "COPC requires LAS point format 6, 7, or 8; found {point_format_id}"
677 )));
678 }
679 let point_data_record_length = reader
680 .read_u16::<LittleEndian>()
681 .map_err(|e| Error::io("read point record length", e))?;
682 reader
683 .seek(SeekFrom::Start(131))
684 .map_err(|e| Error::io("seek LAS transforms", e))?;
685 let x_scale_factor = reader
686 .read_f64::<LittleEndian>()
687 .map_err(|e| Error::io("read x scale factor", e))?;
688 let y_scale_factor = reader
689 .read_f64::<LittleEndian>()
690 .map_err(|e| Error::io("read y scale factor", e))?;
691 let z_scale_factor = reader
692 .read_f64::<LittleEndian>()
693 .map_err(|e| Error::io("read z scale factor", e))?;
694 let x_offset = reader
695 .read_f64::<LittleEndian>()
696 .map_err(|e| Error::io("read x offset", e))?;
697 let y_offset = reader
698 .read_f64::<LittleEndian>()
699 .map_err(|e| Error::io("read y offset", e))?;
700 let z_offset = reader
701 .read_f64::<LittleEndian>()
702 .map_err(|e| Error::io("read z offset", e))?;
703 let max_x = reader
704 .read_f64::<LittleEndian>()
705 .map_err(|e| Error::io("read max x", e))?;
706 let min_x = reader
707 .read_f64::<LittleEndian>()
708 .map_err(|e| Error::io("read min x", e))?;
709 let max_y = reader
710 .read_f64::<LittleEndian>()
711 .map_err(|e| Error::io("read max y", e))?;
712 let min_y = reader
713 .read_f64::<LittleEndian>()
714 .map_err(|e| Error::io("read min y", e))?;
715 let max_z = reader
716 .read_f64::<LittleEndian>()
717 .map_err(|e| Error::io("read max z", e))?;
718 let min_z = reader
719 .read_f64::<LittleEndian>()
720 .map_err(|e| Error::io("read min z", e))?;
721 reader
722 .seek(SeekFrom::Start(235))
723 .map_err(|e| Error::io("seek LAS 1.4 fields", e))?;
724 let offset_to_first_evlr = reader
725 .read_u64::<LittleEndian>()
726 .map_err(|e| Error::io("read first EVLR offset", e))?;
727 let number_of_evlrs = reader
728 .read_u32::<LittleEndian>()
729 .map_err(|e| Error::io("read EVLR count", e))?;
730 if number_of_evlrs > MAX_EVLR_COUNT {
731 return Err(Error::InvalidData(format!(
732 "EVLR count {number_of_evlrs} exceeds max supported {MAX_EVLR_COUNT}"
733 )));
734 }
735 if offset_to_first_evlr != 0 && offset_to_first_evlr > file_len {
736 return Err(Error::InvalidData(format!(
737 "first EVLR offset {offset_to_first_evlr} exceeds file length {file_len}"
738 )));
739 }
740 let number_of_points = reader
741 .read_u64::<LittleEndian>()
742 .map_err(|e| Error::io("read point count", e))?;
743 validate_header_numbers(
744 (x_scale_factor, y_scale_factor, z_scale_factor),
745 (x_offset, y_offset, z_offset),
746 (min_x, min_y, min_z),
747 (max_x, max_y, max_z),
748 )?;
749 reader
750 .seek(SeekFrom::Start(u64::from(header_size)))
751 .map_err(|e| Error::io("seek after LAS header", e))?;
752 Ok(LasHeader {
753 point_data_record_format,
754 point_data_record_length,
755 offset_to_point_data,
756 number_of_vlrs,
757 x_scale_factor,
758 y_scale_factor,
759 z_scale_factor,
760 x_offset,
761 y_offset,
762 z_offset,
763 min_x,
764 max_x,
765 min_y,
766 max_y,
767 min_z,
768 max_z,
769 offset_to_first_evlr,
770 number_of_evlrs,
771 number_of_points,
772 })
773}
774
775fn read_vlrs<R: Read + Seek>(
776 reader: &mut R,
777 count: u32,
778 file_len: u64,
779 section_end: u64,
780) -> Result<Vec<Vlr>> {
781 if count > MAX_VLR_COUNT {
782 return Err(Error::InvalidData(format!(
783 "VLR count {count} exceeds max supported {MAX_VLR_COUNT}"
784 )));
785 }
786 if section_end > file_len {
787 return Err(Error::InvalidData(format!(
788 "VLR section end {section_end} exceeds file length {file_len}"
789 )));
790 }
791 let mut vlrs = Vec::new();
792 for index in 0..count {
793 let header_offset = reader
794 .stream_position()
795 .map_err(|e| Error::io("record VLR offset", e))?;
796 validate_range_in_file(header_offset, VLR_HEADER_BYTES, section_end, "VLR header")?;
797 let reserved = reader
798 .read_u16::<LittleEndian>()
799 .map_err(|e| Error::io("read VLR reserved", e))?;
800 if reserved != 0 {
801 return Err(Error::InvalidData(format!(
802 "VLR {index} reserved field must be zero, got {reserved}"
803 )));
804 }
805 let mut user_id = [0u8; 16];
806 reader
807 .read_exact(&mut user_id)
808 .map_err(|e| Error::io("read VLR user id", e))?;
809 let record_id = reader
810 .read_u16::<LittleEndian>()
811 .map_err(|e| Error::io("read VLR record id", e))?;
812 let record_length = reader
813 .read_u16::<LittleEndian>()
814 .map_err(|e| Error::io("read VLR length", e))?;
815 let mut description = [0u8; 32];
816 reader
817 .read_exact(&mut description)
818 .map_err(|e| Error::io("read VLR description", e))?;
819 let data_offset = reader
820 .stream_position()
821 .map_err(|e| Error::io("record VLR data offset", e))?;
822 let data_end = validate_range_in_file(
823 data_offset,
824 u64::from(record_length),
825 section_end,
826 "VLR data",
827 )?;
828 let user_id_str = trim_nul(&user_id).to_string();
829 if should_store_vlr(&user_id_str, record_id) {
830 let mut data = vec![0u8; usize::from(record_length)];
831 reader
832 .read_exact(&mut data)
833 .map_err(|e| Error::io("read VLR data", e))?;
834 vlrs.push(Vlr {
835 index,
836 user_id: user_id_str,
837 record_id,
838 data,
839 });
840 } else {
841 reader
842 .seek(SeekFrom::Start(data_end))
843 .map_err(|e| Error::io("skip VLR data", e))?;
844 }
845 let actual_next = reader
846 .stream_position()
847 .map_err(|e| Error::io("record next VLR offset", e))?;
848 if actual_next != data_end {
849 return Err(Error::InvalidData(format!(
850 "VLR {index} cursor at {actual_next}, expected {data_end}"
851 )));
852 }
853 }
854 Ok(vlrs)
855}
856
857fn should_store_vlr(user_id: &str, record_id: u16) -> bool {
858 (user_id == "copc" && record_id == 1) || (user_id == "laszip encoded" && record_id == 22204)
859}
860
861fn read_evlr_refs<R: Read + Seek>(
862 reader: &mut R,
863 header: &LasHeader,
864 file_len: u64,
865) -> Result<Vec<EvlrRef>> {
866 if header.offset_to_first_evlr == 0 || header.number_of_evlrs == 0 {
867 return Ok(Vec::new());
868 }
869 if header.number_of_evlrs > MAX_EVLR_COUNT {
870 return Err(Error::InvalidData(format!(
871 "EVLR count {} exceeds max supported {}",
872 header.number_of_evlrs, MAX_EVLR_COUNT
873 )));
874 }
875 validate_range_in_file(
876 header.offset_to_first_evlr,
877 EVLR_HEADER_BYTES,
878 file_len,
879 "first EVLR header",
880 )?;
881 reader
882 .seek(SeekFrom::Start(header.offset_to_first_evlr))
883 .map_err(|e| Error::io("seek EVLRs", e))?;
884 let mut evlrs = Vec::new();
885 for index in 0..header.number_of_evlrs {
886 let header_start = reader
887 .stream_position()
888 .map_err(|e| Error::io("record EVLR offset", e))?;
889 validate_range_in_file(header_start, EVLR_HEADER_BYTES, file_len, "EVLR header")?;
890 let reserved = reader
891 .read_u16::<LittleEndian>()
892 .map_err(|e| Error::io("read EVLR reserved", e))?;
893 if reserved != 0 {
894 return Err(Error::InvalidData(format!(
895 "EVLR {index} reserved field must be zero, got {reserved}"
896 )));
897 }
898 let mut user_id = [0u8; 16];
899 reader
900 .read_exact(&mut user_id)
901 .map_err(|e| Error::io("read EVLR user id", e))?;
902 let record_id = reader
903 .read_u16::<LittleEndian>()
904 .map_err(|e| Error::io("read EVLR record id", e))?;
905 let data_len = reader
906 .read_u64::<LittleEndian>()
907 .map_err(|e| Error::io("read EVLR length", e))?;
908 let mut description = [0u8; 32];
909 reader
910 .read_exact(&mut description)
911 .map_err(|e| Error::io("read EVLR description", e))?;
912 let data_offset = reader
913 .stream_position()
914 .map_err(|e| Error::io("record EVLR data offset", e))?;
915 evlrs.push(EvlrRef {
916 user_id,
917 record_id,
918 data_offset,
919 data_len,
920 });
921 let expected_next = validate_range_in_file(data_offset, data_len, file_len, "EVLR data")?;
922 reader
923 .seek(SeekFrom::Start(expected_next))
924 .map_err(|e| Error::io("skip EVLR data", e))?;
925 let actual_next = reader
926 .stream_position()
927 .map_err(|e| Error::io("record next EVLR offset", e))?;
928 if actual_next != expected_next {
929 return Err(Error::InvalidData(format!(
930 "EVLR {index} cursor at {actual_next}, expected {expected_next}"
931 )));
932 }
933 }
934 Ok(evlrs)
935}
936
937fn validate_header_numbers(
938 scale: (f64, f64, f64),
939 offset: (f64, f64, f64),
940 min: (f64, f64, f64),
941 max: (f64, f64, f64),
942) -> Result<()> {
943 for (axis, value) in [("x", scale.0), ("y", scale.1), ("z", scale.2)] {
944 if !value.is_finite() || value <= 0.0 {
945 return Err(Error::InvalidData(format!(
946 "LAS {axis} scale must be finite and positive, got {value}"
947 )));
948 }
949 }
950 for (name, value) in [
951 ("x offset", offset.0),
952 ("y offset", offset.1),
953 ("z offset", offset.2),
954 ("min x", min.0),
955 ("min y", min.1),
956 ("min z", min.2),
957 ("max x", max.0),
958 ("max y", max.1),
959 ("max z", max.2),
960 ] {
961 if !value.is_finite() {
962 return Err(Error::InvalidData(format!(
963 "LAS {name} must be finite, got {value}"
964 )));
965 }
966 }
967 for (axis, min, max) in [
968 ("x", min.0, max.0),
969 ("y", min.1, max.1),
970 ("z", min.2, max.2),
971 ] {
972 if min > max {
973 return Err(Error::InvalidData(format!(
974 "LAS {axis} minimum {min} exceeds maximum {max}"
975 )));
976 }
977 }
978 Ok(())
979}
980
981fn trim_nul(bytes: &[u8]) -> &str {
982 let end = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
983 std::str::from_utf8(&bytes[..end]).unwrap_or("")
984}
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989
990 use byteorder::{LittleEndian, WriteBytesExt};
991 use copc_core::{EntryAvailability, HIERARCHY_ENTRY_BYTES};
992 use laz::LazVlrBuilder;
993 use std::io::{Cursor, Write};
994
995 #[test]
996 fn hierarchy_walk_loads_recursive_child_pages() {
997 let mut fixture = Cursor::new(copc_with_child_hierarchy_page());
998 let file = CopcFile::from_reader(&mut fixture).unwrap();
999 let child_key = VoxelKey::root().child(3).unwrap();
1000 let grandchild_key = child_key.child(5).unwrap();
1001
1002 assert_eq!(file.root_hierarchy().entries().len(), 2);
1003 assert!(file.root_hierarchy().entries()[1].is_child_page());
1004
1005 let hierarchy = file.hierarchy();
1006 assert_eq!(hierarchy.len(), 3);
1007 assert_eq!(
1008 hierarchy
1009 .get(&VoxelKey::root())
1010 .unwrap()
1011 .availability()
1012 .unwrap(),
1013 EntryAvailability::PointData { point_count: 5 }
1014 );
1015 assert_eq!(
1016 hierarchy.get(&child_key).unwrap().availability().unwrap(),
1017 EntryAvailability::PointData { point_count: 4 }
1018 );
1019 assert_eq!(
1020 hierarchy
1021 .get(&grandchild_key)
1022 .unwrap()
1023 .availability()
1024 .unwrap(),
1025 EntryAvailability::PointData { point_count: 3 }
1026 );
1027 assert!(!hierarchy.values().any(|entry| entry.is_child_page()));
1028
1029 let walk = file.hierarchy_walk();
1030 assert_eq!(walk.len(), hierarchy.len());
1031 assert_eq!(walk.iter().map(|entry| entry.point_count).sum::<i32>(), 12);
1032 }
1033
1034 #[test]
1035 fn rejects_excessive_vlr_count_before_allocation() {
1036 let mut bytes = copc_with_child_hierarchy_page();
1037 put_u32(&mut bytes, 100, MAX_VLR_COUNT + 1);
1038
1039 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1040
1041 assert!(err.to_string().contains("VLR count"));
1042 }
1043
1044 #[test]
1045 fn rejects_excessive_evlr_count_before_allocation() {
1046 let mut bytes = copc_with_child_hierarchy_page();
1047 put_u32(&mut bytes, 243, MAX_EVLR_COUNT + 1);
1048
1049 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1050
1051 assert!(err.to_string().contains("EVLR count"));
1052 }
1053
1054 #[test]
1055 fn rejects_oversized_root_hierarchy_page_before_allocation() {
1056 let mut bytes = copc_with_child_hierarchy_page();
1057 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
1058 put_u64(
1059 &mut bytes,
1060 copc_info_data + 48,
1061 MAX_HIERARCHY_PAGE_BYTES + HIERARCHY_ENTRY_BYTES as u64,
1062 );
1063
1064 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1065
1066 assert!(err.to_string().contains("hierarchy page"));
1067 assert!(err.to_string().contains("max supported"));
1068 }
1069
1070 #[test]
1071 fn rejects_child_hierarchy_page_outside_file() {
1072 let mut bytes = copc_with_child_hierarchy_page();
1073 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
1074 let root_hier_offset = read_u64(&bytes, copc_info_data + 40) as usize;
1075 let child_entry_offset_field = root_hier_offset + HIERARCHY_ENTRY_BYTES + 16;
1076 let outside_file = bytes.len() as u64 + 1;
1077 put_u64(&mut bytes, child_entry_offset_field, outside_file);
1078
1079 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1080
1081 assert!(err.to_string().contains("child hierarchy page"));
1082 assert!(err.to_string().contains("exceeds file length"));
1083 }
1084
1085 #[test]
1086 fn rejects_header_offsets_outside_file_before_allocation() {
1087 for (offset, value, expected) in [
1088 (94, u64::from(u16::MAX), "LAS header size"),
1089 (96, 1, "point data offset"),
1090 (96, u64::MAX, "point data offset"),
1091 (235, u64::MAX, "first EVLR offset"),
1092 ] {
1093 let mut bytes = copc_with_child_hierarchy_page();
1094 put_int(&mut bytes, offset, value);
1095
1096 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1097
1098 assert!(
1099 err.to_string().contains(expected),
1100 "expected {expected:?}, got {err}"
1101 );
1102 }
1103 }
1104
1105 #[test]
1106 fn rejects_vlr_and_evlr_lengths_outside_file_before_allocation() {
1107 let mut bytes = copc_with_child_hierarchy_page();
1108 let first_vlr_length_field = usize::from(LAS_HEADER_SIZE_14) + 20;
1109 put_u16(&mut bytes, first_vlr_length_field, u16::MAX);
1110
1111 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1112
1113 assert!(err.to_string().contains("VLR data"));
1114 assert!(err.to_string().contains("exceeds file length"));
1115
1116 let mut bytes = copc_with_child_hierarchy_page();
1117 let evlr_start = read_u64(&bytes, 235) as usize;
1118 let evlr_length_field = evlr_start + 20;
1119 put_u64(&mut bytes, evlr_length_field, u64::MAX);
1120
1121 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1122
1123 assert!(err.to_string().contains("EVLR data"));
1124 assert!(
1125 err.to_string().contains("overflow") || err.to_string().contains("exceeds file length")
1126 );
1127 }
1128
1129 #[test]
1130 fn rejects_malformed_root_hierarchy_sizes() {
1131 for (root_hier_size, expected) in [
1132 (0, "empty"),
1133 (HIERARCHY_ENTRY_BYTES as u64 - 1, "not a multiple"),
1134 ] {
1135 let mut bytes = copc_with_child_hierarchy_page();
1136 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
1137 put_u64(&mut bytes, copc_info_data + 48, root_hier_size);
1138
1139 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1140
1141 assert!(
1142 err.to_string().contains(expected),
1143 "expected {expected:?}, got {err}"
1144 );
1145 }
1146 }
1147
1148 #[test]
1149 fn rejects_invalid_hierarchy_entry_byte_sizes() {
1150 let mut bytes = copc_with_child_hierarchy_page();
1151 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
1152 let root_hier_offset = read_u64(&bytes, copc_info_data + 40) as usize;
1153 put_i32(&mut bytes, root_hier_offset + 24, 0);
1154
1155 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1156
1157 assert!(err.to_string().contains("point data entry"));
1158 assert!(err.to_string().contains("invalid byte size"));
1159
1160 let mut bytes = copc_with_child_hierarchy_page();
1161 let root_hier_offset = read_u64(&bytes, copc_info_data + 40) as usize;
1162 put_i32(&mut bytes, root_hier_offset + HIERARCHY_ENTRY_BYTES + 24, 0);
1163
1164 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1165
1166 assert!(err.to_string().contains("child hierarchy page"));
1167 assert!(err.to_string().contains("invalid byte size"));
1168 }
1169
1170 #[test]
1171 fn deep_child_page_chain_loads_without_stack_overflow() {
1172 let depth = 200_000;
1174 let mut fixture = Cursor::new(copc_with_deep_child_page_chain(depth));
1175
1176 let file = CopcFile::from_reader(&mut fixture).unwrap();
1177
1178 assert_eq!(file.hierarchy().len(), depth);
1179 }
1180
1181 #[test]
1182 fn rejects_hierarchy_point_total_exceeding_header_count() {
1183 let mut bytes = copc_with_child_hierarchy_page();
1185 put_u64(&mut bytes, 247, 11);
1186
1187 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1188
1189 assert!(err.to_string().contains("hierarchy point total"));
1190 }
1191
1192 #[test]
1193 fn rejects_overlapping_point_chunks() {
1194 let mut bytes = copc_with_child_hierarchy_page();
1195 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
1196 let root_hier_offset = read_u64(&bytes, copc_info_data + 40) as usize;
1197 let root_point_offset = read_u64(&bytes, root_hier_offset + 16);
1198 let child_page_offset =
1199 read_u64(&bytes, root_hier_offset + HIERARCHY_ENTRY_BYTES + 16) as usize;
1200 put_u64(&mut bytes, child_page_offset + 16, root_point_offset + 50);
1201
1202 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1203
1204 assert!(err.to_string().contains("point chunks"));
1205 assert!(err.to_string().contains("overlap"));
1206 }
1207
1208 #[test]
1209 fn rejects_huge_claimed_point_count_before_allocation() {
1210 let mut bytes = copc_with_child_hierarchy_page();
1211 let copc_info_data = usize::from(LAS_HEADER_SIZE_14) + VLR_HEADER_BYTES as usize;
1212 let root_hier_offset = read_u64(&bytes, copc_info_data + 40) as usize;
1213 put_i32(&mut bytes, root_hier_offset + 28, i32::MAX);
1214
1215 let err = CopcFile::from_reader(&mut Cursor::new(bytes)).unwrap_err();
1216
1217 assert!(err.to_string().contains("hierarchy point total"));
1218 }
1219
1220 #[test]
1221 fn truncated_inputs_fail_without_panicking() {
1222 let bytes = copc_with_child_hierarchy_page();
1223 for len in [
1224 0,
1225 1,
1226 4,
1227 128,
1228 usize::from(LAS_HEADER_SIZE_14) - 1,
1229 usize::from(LAS_HEADER_SIZE_14),
1230 bytes.len() / 2,
1231 bytes.len() - 1,
1232 ] {
1233 let truncated = bytes[..len].to_vec();
1234
1235 let err = CopcFile::from_reader(&mut Cursor::new(truncated)).unwrap_err();
1236
1237 assert!(
1238 !err.to_string().is_empty(),
1239 "truncated input length {len} produced an empty error"
1240 );
1241 }
1242 }
1243
1244 fn copc_with_child_hierarchy_page() -> Vec<u8> {
1245 let mut laz_vlr_bytes = Vec::new();
1246 LazVlrBuilder::default()
1247 .with_point_format(6, 0)
1248 .unwrap()
1249 .with_variable_chunk_size()
1250 .build()
1251 .write_to(&mut laz_vlr_bytes)
1252 .unwrap();
1253
1254 let offset_to_point_data = u32::from(LAS_HEADER_SIZE_14)
1255 + (54 + copc_core::info::COPC_INFO_BYTES as u32)
1256 + (54 + laz_vlr_bytes.len() as u32);
1257 let root_point_offset = u64::from(offset_to_point_data);
1258 let child_point_offset = root_point_offset + 100;
1259 let grandchild_point_offset = child_point_offset + 200;
1260 let evlr_start = grandchild_point_offset + 220;
1261 let root_hier_offset = evlr_start + 60;
1262 let root_hier_size = (2 * HIERARCHY_ENTRY_BYTES) as u64;
1263 let child_page_offset = root_hier_offset + root_hier_size;
1264
1265 let child_key = VoxelKey::root().child(3).unwrap();
1266 let grandchild_key = child_key.child(5).unwrap();
1267 let child_page = HierarchyPage::new(vec![
1268 Entry {
1269 key: child_key,
1270 offset: child_point_offset,
1271 byte_size: 200,
1272 point_count: 4,
1273 },
1274 Entry {
1275 key: grandchild_key,
1276 offset: grandchild_point_offset,
1277 byte_size: 220,
1278 point_count: 3,
1279 },
1280 ]);
1281 let child_page_bytes = child_page.write_le_bytes().unwrap();
1282 let root_page = HierarchyPage::new(vec![
1283 Entry {
1284 key: VoxelKey::root(),
1285 offset: root_point_offset,
1286 byte_size: 100,
1287 point_count: 5,
1288 },
1289 Entry {
1290 key: child_key,
1291 offset: child_page_offset,
1292 byte_size: child_page_bytes.len() as i32,
1293 point_count: -1,
1294 },
1295 ]);
1296 let root_page_bytes = root_page.write_le_bytes().unwrap();
1297
1298 let info = CopcInfo {
1299 center: (0.0, 0.0, 0.0),
1300 halfsize: 10.0,
1301 spacing: 1.0,
1302 root_hier_offset,
1303 root_hier_size,
1304 gpstime_min: 0.0,
1305 gpstime_max: 0.0,
1306 };
1307
1308 let mut out = Vec::new();
1309 write_las_header(&mut out, offset_to_point_data, evlr_start, 12);
1310 write_vlr(
1311 &mut out,
1312 "copc",
1313 1,
1314 &info.write_le_bytes().unwrap(),
1315 "COPC info",
1316 );
1317 write_vlr(
1318 &mut out,
1319 "laszip encoded",
1320 22204,
1321 &laz_vlr_bytes,
1322 "http://laszip.org",
1323 );
1324 assert_eq!(out.len(), offset_to_point_data as usize);
1325 out.resize(evlr_start as usize, 0);
1326
1327 write_evlr_header(
1328 &mut out,
1329 "copc",
1330 1000,
1331 (root_page_bytes.len() + child_page_bytes.len()) as u64,
1332 "COPC hierarchy",
1333 );
1334 assert_eq!(out.len() as u64, root_hier_offset);
1335 out.extend_from_slice(&root_page_bytes);
1336 assert_eq!(out.len() as u64, child_page_offset);
1337 out.extend_from_slice(&child_page_bytes);
1338 out
1339 }
1340
1341 fn copc_with_deep_child_page_chain(depth: usize) -> Vec<u8> {
1344 assert!(depth >= 2);
1345 let mut laz_vlr_bytes = Vec::new();
1346 LazVlrBuilder::default()
1347 .with_point_format(6, 0)
1348 .unwrap()
1349 .with_variable_chunk_size()
1350 .build()
1351 .write_to(&mut laz_vlr_bytes)
1352 .unwrap();
1353
1354 let offset_to_point_data = u32::from(LAS_HEADER_SIZE_14)
1355 + (54 + copc_core::info::COPC_INFO_BYTES as u32)
1356 + (54 + laz_vlr_bytes.len() as u32);
1357 let evlr_start = u64::from(offset_to_point_data);
1358 let root_hier_offset = evlr_start + 60;
1359 let page_offset = |page: usize| root_hier_offset + (page * HIERARCHY_ENTRY_BYTES) as u64;
1360
1361 let info = CopcInfo {
1362 center: (0.0, 0.0, 0.0),
1363 halfsize: 10.0,
1364 spacing: 1.0,
1365 root_hier_offset,
1366 root_hier_size: HIERARCHY_ENTRY_BYTES as u64,
1367 gpstime_min: 0.0,
1368 gpstime_max: 0.0,
1369 };
1370
1371 let mut out = Vec::new();
1372 write_las_header(&mut out, offset_to_point_data, evlr_start, 0);
1373 write_vlr(
1374 &mut out,
1375 "copc",
1376 1,
1377 &info.write_le_bytes().unwrap(),
1378 "COPC info",
1379 );
1380 write_vlr(
1381 &mut out,
1382 "laszip encoded",
1383 22204,
1384 &laz_vlr_bytes,
1385 "http://laszip.org",
1386 );
1387 assert_eq!(out.len(), offset_to_point_data as usize);
1388 write_evlr_header(
1389 &mut out,
1390 "copc",
1391 1000,
1392 (depth * HIERARCHY_ENTRY_BYTES) as u64,
1393 "COPC hierarchy",
1394 );
1395 assert_eq!(out.len() as u64, root_hier_offset);
1396 for page in 0..depth {
1397 let entry = if page + 1 < depth {
1398 Entry {
1399 key: VoxelKey {
1400 level: (page + 1) as i32,
1401 x: 0,
1402 y: 0,
1403 z: 0,
1404 },
1405 offset: page_offset(page + 1),
1406 byte_size: HIERARCHY_ENTRY_BYTES as i32,
1407 point_count: -1,
1408 }
1409 } else {
1410 Entry {
1411 key: VoxelKey {
1412 level: depth as i32,
1413 x: 0,
1414 y: 0,
1415 z: 0,
1416 },
1417 offset: 0,
1418 byte_size: 0,
1419 point_count: 0,
1420 }
1421 };
1422 let mut entry_bytes = [0u8; HIERARCHY_ENTRY_BYTES];
1423 entry.write_le(&mut entry_bytes).unwrap();
1424 out.extend_from_slice(&entry_bytes);
1425 }
1426 out
1427 }
1428
1429 fn write_las_header(
1430 out: &mut Vec<u8>,
1431 offset_to_point_data: u32,
1432 evlr_start: u64,
1433 point_count: u64,
1434 ) {
1435 out.resize(usize::from(LAS_HEADER_SIZE_14), 0);
1436 out[0..4].copy_from_slice(b"LASF");
1437 put_u16(out, 6, 0x10);
1438 out[24] = 1;
1439 out[25] = 4;
1440 put_u16(out, 94, LAS_HEADER_SIZE_14);
1441 put_u32(out, 96, offset_to_point_data);
1442 put_u32(out, 100, 2);
1443 out[104] = 6 | 0x80;
1444 put_u16(out, 105, 30);
1445 put_f64(out, 131, 0.001);
1446 put_f64(out, 139, 0.001);
1447 put_f64(out, 147, 0.001);
1448 put_f64(out, 155, 0.0);
1449 put_f64(out, 163, 0.0);
1450 put_f64(out, 171, 0.0);
1451 put_f64(out, 179, 10.0);
1452 put_f64(out, 187, -10.0);
1453 put_f64(out, 195, 10.0);
1454 put_f64(out, 203, -10.0);
1455 put_f64(out, 211, 10.0);
1456 put_f64(out, 219, -10.0);
1457 put_u64(out, 235, evlr_start);
1458 put_u32(out, 243, 1);
1459 put_u64(out, 247, point_count);
1460 }
1461
1462 fn write_vlr(out: &mut Vec<u8>, user_id: &str, record_id: u16, data: &[u8], desc: &str) {
1463 out.write_u16::<LittleEndian>(0).unwrap();
1464 out.write_all(&padded(user_id.as_bytes(), 16)).unwrap();
1465 out.write_u16::<LittleEndian>(record_id).unwrap();
1466 out.write_u16::<LittleEndian>(data.len() as u16).unwrap();
1467 out.write_all(&padded(desc.as_bytes(), 32)).unwrap();
1468 out.write_all(data).unwrap();
1469 }
1470
1471 fn write_evlr_header(
1472 out: &mut Vec<u8>,
1473 user_id: &str,
1474 record_id: u16,
1475 data_len: u64,
1476 desc: &str,
1477 ) {
1478 out.write_u16::<LittleEndian>(0).unwrap();
1479 out.write_all(&padded(user_id.as_bytes(), 16)).unwrap();
1480 out.write_u16::<LittleEndian>(record_id).unwrap();
1481 out.write_u64::<LittleEndian>(data_len).unwrap();
1482 out.write_all(&padded(desc.as_bytes(), 32)).unwrap();
1483 }
1484
1485 fn padded(bytes: &[u8], len: usize) -> Vec<u8> {
1486 let mut out = vec![0u8; len];
1487 let count = bytes.len().min(len);
1488 out[..count].copy_from_slice(&bytes[..count]);
1489 out
1490 }
1491
1492 fn put_u16(out: &mut [u8], offset: usize, value: u16) {
1493 out[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
1494 }
1495
1496 fn put_u32(out: &mut [u8], offset: usize, value: u32) {
1497 out[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1498 }
1499
1500 fn put_u64(out: &mut [u8], offset: usize, value: u64) {
1501 out[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1502 }
1503
1504 fn put_i32(out: &mut [u8], offset: usize, value: i32) {
1505 out[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1506 }
1507
1508 fn put_int(out: &mut [u8], offset: usize, value: u64) {
1509 match offset {
1510 94 => put_u16(out, offset, value as u16),
1511 96 => put_u32(out, offset, value as u32),
1512 235 => put_u64(out, offset, value),
1513 _ => unreachable!("unexpected integer offset"),
1514 }
1515 }
1516
1517 fn read_u64(bytes: &[u8], offset: usize) -> u64 {
1518 u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap())
1519 }
1520
1521 fn put_f64(out: &mut [u8], offset: usize, value: f64) {
1522 out[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1523 }
1524}