Skip to main content

copc_reader/
lib.rs

1//! Pure-Rust COPC reader.
2//!
3//! Parses LAS/COPC metadata and exposes chunked-LAZ point iteration over COPC
4//! hierarchy entries.
5
6#![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/// A parsed COPC file.
42#[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/// Minimal LAS header fields needed by COPC callers.
52#[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    /// Return all parsed hierarchy entries, including entries from child pages.
179    pub fn hierarchy_walk(&self) -> Vec<Entry> {
180        self.hierarchy.values().copied().collect()
181    }
182
183    /// Return the full hierarchy index keyed by COPC voxel key.
184    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
302/// Extracts the COPC info and LASzip VLRs every COPC file must carry.
303pub(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
391/// Loads the full hierarchy tree with an explicit worklist rather than
392/// recursion, so a crafted file with a long chain of child pages cannot
393/// overflow the stack.
394fn 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
465/// COPC distributes every point to exactly one hierarchy node, so the sum of
466/// node counts must equal the LAS 1.4 extended point count.
467fn 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        // 200k chained pages would overflow the stack under recursive loading.
1173        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        // Entries sum to 12 points; lower the header count to 11.
1184        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    /// A COPC file whose hierarchy is a linear chain of `depth` single-entry
1342    /// pages, each pointing at the next; the final page holds one empty entry.
1343    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}