copc-core 0.9.0

Shared COPC metadata, hierarchy, streaming point, and error types
Documentation
use crate::{Error, Result, HIERARCHY_ENTRY_BYTES};

pub const COPC_INFO_BYTES: usize = 160;

/// COPC info VLR payload.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CopcInfo {
    pub center: (f64, f64, f64),
    pub halfsize: f64,
    pub spacing: f64,
    pub root_hier_offset: u64,
    pub root_hier_size: u64,
    pub gpstime_min: f64,
    pub gpstime_max: f64,
}

impl CopcInfo {
    pub fn from_le_bytes(bytes: &[u8]) -> Result<Self> {
        if bytes.len() != COPC_INFO_BYTES {
            return Err(Error::InvalidData(format!(
                "COPC info payload is {} bytes, expected exactly {}",
                bytes.len(),
                COPC_INFO_BYTES
            )));
        }
        if bytes[72..].iter().any(|byte| *byte != 0) {
            return Err(Error::InvalidData(
                "COPC info reserved bytes must all be zero".into(),
            ));
        }
        let info = Self {
            center: (read_f64(bytes, 0), read_f64(bytes, 8), read_f64(bytes, 16)),
            halfsize: read_f64(bytes, 24),
            spacing: read_f64(bytes, 32),
            root_hier_offset: read_u64(bytes, 40),
            root_hier_size: read_u64(bytes, 48),
            gpstime_min: read_f64(bytes, 56),
            gpstime_max: read_f64(bytes, 64),
        };
        info.validate()?;
        Ok(info)
    }

    /// Validate and serialize the exact 160-byte COPC info payload.
    pub fn write_le_bytes(self) -> Result<[u8; COPC_INFO_BYTES]> {
        self.validate()?;
        let mut out = [0u8; COPC_INFO_BYTES];
        write_f64(&mut out, 0, self.center.0);
        write_f64(&mut out, 8, self.center.1);
        write_f64(&mut out, 16, self.center.2);
        write_f64(&mut out, 24, self.halfsize);
        write_f64(&mut out, 32, self.spacing);
        write_u64(&mut out, 40, self.root_hier_offset);
        write_u64(&mut out, 48, self.root_hier_size);
        write_f64(&mut out, 56, self.gpstime_min);
        write_f64(&mut out, 64, self.gpstime_max);
        Ok(out)
    }

    pub fn validate(self) -> Result<()> {
        for (name, value) in [
            ("center x", self.center.0),
            ("center y", self.center.1),
            ("center z", self.center.2),
        ] {
            if !value.is_finite() {
                return Err(Error::InvalidData(format!(
                    "COPC info {name} must be finite, got {value}"
                )));
            }
        }
        for (name, value) in [("halfsize", self.halfsize), ("spacing", self.spacing)] {
            if !value.is_finite() || value <= 0.0 {
                return Err(Error::InvalidData(format!(
                    "COPC info {name} must be finite and positive, got {value}"
                )));
            }
        }
        if self.root_hier_offset == 0 {
            return Err(Error::InvalidData(
                "COPC root hierarchy offset must be non-zero".into(),
            ));
        }
        if self.root_hier_size == 0 {
            return Err(Error::InvalidData(
                "COPC root hierarchy page is empty".into(),
            ));
        }
        if !self
            .root_hier_size
            .is_multiple_of(HIERARCHY_ENTRY_BYTES as u64)
        {
            return Err(Error::InvalidData(format!(
                "COPC root hierarchy size {} is not a multiple of {HIERARCHY_ENTRY_BYTES}",
                self.root_hier_size
            )));
        }
        if !self.gpstime_min.is_finite() || !self.gpstime_max.is_finite() {
            return Err(Error::InvalidData(format!(
                "COPC GPS time range must be finite, got {}..{}",
                self.gpstime_min, self.gpstime_max
            )));
        }
        if self.gpstime_min > self.gpstime_max {
            return Err(Error::InvalidData(format!(
                "COPC GPS time minimum {} exceeds maximum {}",
                self.gpstime_min, self.gpstime_max
            )));
        }
        Ok(())
    }
}

fn read_u64(bytes: &[u8], offset: usize) -> u64 {
    u64::from_le_bytes(
        bytes[offset..offset + 8]
            .try_into()
            .expect("u64 width checked by caller"),
    )
}

fn read_f64(bytes: &[u8], offset: usize) -> f64 {
    f64::from_le_bytes(
        bytes[offset..offset + 8]
            .try_into()
            .expect("f64 width checked by caller"),
    )
}

fn write_u64(bytes: &mut [u8], offset: usize, value: u64) {
    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}

fn write_f64(bytes: &mut [u8], offset: usize, value: f64) {
    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn copc_info_round_trips() {
        let info = CopcInfo {
            center: (1.0, 2.0, 3.0),
            halfsize: 4.0,
            spacing: 0.25,
            root_hier_offset: 100,
            root_hier_size: 320,
            gpstime_min: 10.0,
            gpstime_max: 20.0,
        };
        assert_eq!(
            CopcInfo::from_le_bytes(&info.write_le_bytes().unwrap()).unwrap(),
            info
        );
    }

    #[test]
    fn copc_info_rejects_nonzero_reserved_bytes_and_invalid_numbers() {
        let info = CopcInfo {
            center: (1.0, 2.0, 3.0),
            halfsize: 4.0,
            spacing: 0.25,
            root_hier_offset: 100,
            root_hier_size: 32,
            gpstime_min: 10.0,
            gpstime_max: 20.0,
        };
        let mut bytes = info.write_le_bytes().unwrap();
        bytes[159] = 1;
        assert!(CopcInfo::from_le_bytes(&bytes).is_err());

        let mut bytes = info.write_le_bytes().unwrap();
        write_f64(&mut bytes, 24, f64::NAN);
        assert!(CopcInfo::from_le_bytes(&bytes).is_err());

        let mut bytes = info.write_le_bytes().unwrap();
        write_f64(&mut bytes, 56, 21.0);
        assert!(CopcInfo::from_le_bytes(&bytes).is_err());
    }
}