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