1use std::collections::{BTreeMap, HashSet};
8use std::io::{Read, Seek, SeekFrom};
9
10use copc_core::{
11 ColumnSelection, CopcInfo, Entry, EntryAvailability, Error, HierarchyPage, LasColumnBatch,
12 Result, VoxelKey,
13};
14use las::Point;
15use laz::LazVlr;
16
17use crate::points::{
18 decode_chunk_points, select_point_chunks_from, selected_column_builders,
19 total_candidate_points, validate_query, voxel_bounds, BoundsSelection, ChunkColumnDecoder,
20 PointQuery, MAX_INITIAL_COLUMN_RESERVE_POINTS,
21};
22use crate::range_read::RangeRead;
23use crate::{
24 extract_required_vlrs, insert_point_chunk_range, point_format_for, read_las_header, read_vlrs,
25 transforms_for, validate_hierarchy_entry, validate_hierarchy_page_byte_size,
26 validate_range_in_file, CopcDataRanges, HierarchyReadLimits, LasHeader, LAS_HEADER_SIZE_14,
27};
28
29const RANGE_COALESCE_GAP_BYTES: u64 = 64 * 1024;
31const MAX_COALESCED_RANGE_BYTES: u64 = 64 * 1024 * 1024;
33const MAX_VLR_SECTION_BYTES: u64 = 64 * 1024 * 1024;
35
36struct SectionReader {
39 base: u64,
40 data: Vec<u8>,
41 position: u64,
42}
43
44impl SectionReader {
45 fn new(base: u64, data: Vec<u8>) -> Self {
46 Self {
47 base,
48 data,
49 position: base,
50 }
51 }
52}
53
54impl Read for SectionReader {
55 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
56 let start = usize::try_from(self.position.checked_sub(self.base).ok_or_else(|| {
57 std::io::Error::new(
58 std::io::ErrorKind::InvalidInput,
59 "read before fetched section",
60 )
61 })?)
62 .map_err(|_| {
63 std::io::Error::new(
64 std::io::ErrorKind::InvalidInput,
65 "fetched section position exceeds usize",
66 )
67 })?;
68 if start > self.data.len() {
69 return Err(std::io::Error::new(
70 std::io::ErrorKind::UnexpectedEof,
71 "read past fetched section",
72 ));
73 }
74 let available = &self.data[start..];
75 let take = available.len().min(buf.len());
76 buf[..take].copy_from_slice(&available[..take]);
77 self.position += take as u64;
78 Ok(take)
79 }
80}
81
82impl Seek for SectionReader {
83 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
84 let target = match pos {
85 SeekFrom::Start(offset) => Some(offset),
86 SeekFrom::Current(delta) => self.position.checked_add_signed(delta),
87 SeekFrom::End(delta) => (self.base + self.data.len() as u64).checked_add_signed(delta),
88 }
89 .ok_or_else(|| {
90 std::io::Error::new(std::io::ErrorKind::InvalidInput, "seek out of range")
91 })?;
92 self.position = target;
93 Ok(self.position)
94 }
95}
96
97pub struct CopcRangeReader<S: RangeRead> {
104 source: S,
105 file_len: u64,
106 header: LasHeader,
107 copc_info: CopcInfo,
108 laszip_vlr: LazVlr,
109 hierarchy: BTreeMap<VoxelKey, Entry>,
110 pending_pages: Vec<Entry>,
111 visited_pages: HashSet<(u64, u64)>,
112 limits: HierarchyReadLimits,
113 loaded_point_total: u64,
114 data_ranges: CopcDataRanges,
115 point_ranges: BTreeMap<u64, (u64, VoxelKey)>,
116}
117
118impl<S: RangeRead> CopcRangeReader<S> {
119 pub fn open(mut source: S) -> Result<Self> {
120 let file_len = source.len()?;
121 if file_len < u64::from(LAS_HEADER_SIZE_14) {
122 return Err(Error::InvalidData(format!(
123 "file is {file_len} bytes; COPC requires at least {LAS_HEADER_SIZE_14}"
124 )));
125 }
126 let mut header_bytes = vec![0u8; usize::from(LAS_HEADER_SIZE_14)];
127 source.read_range(0, &mut header_bytes)?;
128 let mut header_section = SectionReader::new(0, header_bytes);
129 let header = read_las_header(&mut header_section, file_len)?;
130 let header_size = header_section
131 .stream_position()
132 .map_err(|e| Error::io("record header size", e))?;
133
134 let vlr_section_len = u64::from(header.offset_to_point_data)
135 .checked_sub(header_size)
136 .ok_or_else(|| Error::InvalidData("VLR section precedes LAS header end".into()))?;
137 if vlr_section_len > MAX_VLR_SECTION_BYTES {
138 return Err(Error::InvalidData(format!(
139 "VLR section is {vlr_section_len} bytes, max supported is {MAX_VLR_SECTION_BYTES}"
140 )));
141 }
142 let mut vlr_bytes = vec![0u8; vlr_section_len as usize];
143 source.read_range(header_size, &mut vlr_bytes)?;
144 let mut vlr_section = SectionReader::new(header_size, vlr_bytes);
145 let vlrs = read_vlrs(
146 &mut vlr_section,
147 header.number_of_vlrs,
148 file_len,
149 u64::from(header.offset_to_point_data),
150 )?;
151 let (copc_info, laszip_vlr) = extract_required_vlrs(&vlrs)?;
152 validate_hierarchy_page_byte_size(copc_info.root_hier_size)?;
153 let (hierarchy_start, hierarchy_len) =
154 validate_root_hierarchy_evlr(&mut source, file_len, &copc_info)?;
155 let data_ranges = CopcDataRanges::new(&header, hierarchy_start, hierarchy_len)?;
156
157 let mut reader = Self {
158 source,
159 file_len,
160 header,
161 copc_info,
162 laszip_vlr,
163 hierarchy: BTreeMap::new(),
164 pending_pages: Vec::new(),
165 visited_pages: HashSet::new(),
166 limits: HierarchyReadLimits::default(),
167 loaded_point_total: 0,
168 data_ranges,
169 point_ranges: BTreeMap::new(),
170 };
171 let root = Entry {
172 key: VoxelKey::root(),
173 offset: reader.copc_info.root_hier_offset,
174 byte_size: i32::try_from(reader.copc_info.root_hier_size)
175 .map_err(|_| Error::InvalidData("root hierarchy size exceeds i32".into()))?,
176 point_count: -1,
177 };
178 reader.load_page(root)?;
179 Ok(reader)
180 }
181
182 pub fn header(&self) -> &LasHeader {
183 &self.header
184 }
185
186 pub fn copc_info(&self) -> &CopcInfo {
187 &self.copc_info
188 }
189
190 pub fn hierarchy_for(&mut self, query: PointQuery) -> Result<Vec<Entry>> {
193 validate_query(query)?;
194 self.ensure_hierarchy_for(query)?;
195 select_point_chunks_from(self.hierarchy.values(), &self.copc_info, query)
196 }
197
198 pub fn read_columns(
200 &mut self,
201 query: PointQuery,
202 selection: ColumnSelection,
203 ) -> Result<LasColumnBatch> {
204 let chunks = self.hierarchy_for(query)?;
205 let point_format = point_format_for(&self.header)?;
206 let bounds = match query.bounds {
207 BoundsSelection::All => None,
208 BoundsSelection::Within(bounds) => Some(bounds),
209 };
210 let decoder = ChunkColumnDecoder::new(
211 self.laszip_vlr.clone(),
212 point_format,
213 transforms_for(&self.header),
214 bounds,
215 )?;
216 let capacity = match bounds {
217 Some(_) => 0,
218 None => total_candidate_points(&chunks)?.min(MAX_INITIAL_COLUMN_RESERVE_POINTS),
219 };
220 let mut columns = selected_column_builders(point_format, selection, capacity)?;
221 let mut accepted_points = 0usize;
222 for group in coalesce_chunks(&chunks, RANGE_COALESCE_GAP_BYTES, MAX_COALESCED_RANGE_BYTES) {
223 let bytes = self.fetch_range(group.start, group.end)?;
224 for entry in group.entries {
225 let (slice, points_in_chunk) = chunk_slice(&bytes, group.start, entry)?;
226 accepted_points +=
227 decoder.decode_into(slice, points_in_chunk, &mut columns, None)?;
228 }
229 }
230 let batch = LasColumnBatch {
231 len: accepted_points,
232 columns,
233 };
234 batch.validate()?;
235 Ok(batch)
236 }
237
238 pub fn read_points(&mut self, query: PointQuery) -> Result<Vec<Point>> {
240 let chunks = self.hierarchy_for(query)?;
241 let point_format = point_format_for(&self.header)?;
242 let transforms = transforms_for(&self.header);
243 let bounds = match query.bounds {
244 BoundsSelection::All => None,
245 BoundsSelection::Within(bounds) => Some(bounds),
246 };
247 let capacity = match bounds {
248 Some(_) => 0,
249 None => total_candidate_points(&chunks)?.min(MAX_INITIAL_COLUMN_RESERVE_POINTS),
250 };
251 let mut points = Vec::with_capacity(capacity);
252 for group in coalesce_chunks(&chunks, RANGE_COALESCE_GAP_BYTES, MAX_COALESCED_RANGE_BYTES) {
253 let bytes = self.fetch_range(group.start, group.end)?;
254 for entry in group.entries {
255 let (slice, points_in_chunk) = chunk_slice(&bytes, group.start, entry)?;
256 decode_chunk_points(
257 slice,
258 points_in_chunk,
259 &self.laszip_vlr,
260 &point_format,
261 &transforms,
262 bounds,
263 &mut points,
264 )?;
265 }
266 }
267 Ok(points)
268 }
269
270 fn fetch_range(&mut self, start: u64, end: u64) -> Result<Vec<u8>> {
271 let len = usize::try_from(
272 end.checked_sub(start)
273 .ok_or_else(|| Error::InvalidData("chunk range end precedes its start".into()))?,
274 )
275 .map_err(|_| Error::InvalidData("chunk range exceeds usize".into()))?;
276 let mut bytes = vec![0u8; len];
277 self.source.read_range(start, &mut bytes)?;
278 Ok(bytes)
279 }
280
281 fn ensure_hierarchy_for(&mut self, query: PointQuery) -> Result<()> {
283 loop {
284 let mut relevant = Vec::new();
285 let mut rest = Vec::new();
286 for entry in self.pending_pages.drain(..) {
287 if page_may_match(entry.key, &self.copc_info, query)? {
288 relevant.push(entry);
289 } else {
290 rest.push(entry);
291 }
292 }
293 self.pending_pages = rest;
294 if relevant.is_empty() {
295 return Ok(());
296 }
297 for entry in relevant {
298 self.load_page(entry)?;
299 }
300 }
301 }
302
303 fn load_page(&mut self, page_entry: Entry) -> Result<()> {
304 let byte_size = u64::try_from(page_entry.byte_size).map_err(|_| {
305 Error::InvalidData(format!(
306 "child hierarchy page {:?} has negative byte size {}",
307 page_entry.key, page_entry.byte_size
308 ))
309 })?;
310 if !self.visited_pages.insert((page_entry.offset, byte_size)) {
311 return Ok(());
312 }
313 if byte_size == 0 || !byte_size.is_multiple_of(copc_core::HIERARCHY_ENTRY_BYTES as u64) {
314 return Err(Error::InvalidData(format!(
315 "hierarchy page is {byte_size} bytes, not a multiple of {}",
316 copc_core::HIERARCHY_ENTRY_BYTES
317 )));
318 }
319 self.limits.add_page(byte_size)?;
320 validate_range_in_file(
321 page_entry.offset,
322 byte_size,
323 self.file_len,
324 "hierarchy page",
325 )?;
326 let bytes = self.fetch_range(page_entry.offset, page_entry.offset + byte_size)?;
327 let page = HierarchyPage::from_le_bytes(&bytes)?;
328 for entry in page.entries().iter().copied() {
329 validate_hierarchy_entry(entry, self.file_len, &self.data_ranges)?;
330 match self.hierarchy.get(&entry.key).copied() {
331 None => {}
332 Some(previous) if previous.is_child_page() && !entry.is_child_page() => {}
333 Some(previous) => {
334 return Err(Error::InvalidData(format!(
335 "duplicate hierarchy key {:?}: previous {previous:?}, new {entry:?}",
336 entry.key
337 )));
338 }
339 }
340 if entry.has_point_data() {
341 insert_point_chunk_range(&mut self.point_ranges, entry)?;
342 }
343 self.hierarchy.insert(entry.key, entry);
344 if let EntryAvailability::PointData { point_count } = entry.availability()? {
345 self.loaded_point_total = self
346 .loaded_point_total
347 .checked_add(u64::from(point_count))
348 .ok_or_else(|| Error::InvalidData("hierarchy point total overflow".into()))?;
349 }
350 if entry.is_child_page() {
351 self.pending_pages.push(entry);
352 }
353 }
354 if self.loaded_point_total > self.header.number_of_points {
355 return Err(Error::InvalidData(format!(
356 "hierarchy point total {} exceeds LAS header point count {}",
357 self.loaded_point_total, self.header.number_of_points
358 )));
359 }
360 if self.pending_pages.is_empty() && self.loaded_point_total != self.header.number_of_points
361 {
362 return Err(Error::InvalidData(format!(
363 "hierarchy point total {} does not match LAS header point count {}",
364 self.loaded_point_total, self.header.number_of_points
365 )));
366 }
367 Ok(())
368 }
369}
370
371fn page_may_match(key: VoxelKey, info: &CopcInfo, query: PointQuery) -> Result<bool> {
375 let (_, level_max) = crate::points::level_range(query.lod, info)?;
376 if key.level >= level_max {
377 return Ok(false);
378 }
379 if let BoundsSelection::Within(bounds) = query.bounds {
380 if key.level >= 0 && key.x >= 0 && key.y >= 0 && key.z >= 0 {
381 return Ok(voxel_bounds(key, info)?.intersects(bounds));
382 }
383 }
384 Ok(true)
385}
386
387struct ChunkGroup {
388 start: u64,
389 end: u64,
390 entries: Vec<Entry>,
391}
392
393fn coalesce_chunks(chunks: &[Entry], gap: u64, max_range_bytes: u64) -> Vec<ChunkGroup> {
395 let mut groups: Vec<ChunkGroup> = Vec::new();
396 for entry in chunks {
397 let start = entry.offset;
398 let end = entry.offset + entry.byte_size.max(0) as u64;
399 match groups.last_mut() {
400 Some(group)
401 if start <= group.end.saturating_add(gap)
402 && end.saturating_sub(group.start) <= max_range_bytes =>
403 {
404 group.end = group.end.max(end);
405 group.entries.push(*entry);
406 }
407 _ => groups.push(ChunkGroup {
408 start,
409 end,
410 entries: vec![*entry],
411 }),
412 }
413 }
414 groups
415}
416
417fn validate_root_hierarchy_evlr<S: RangeRead>(
418 source: &mut S,
419 file_len: u64,
420 info: &CopcInfo,
421) -> Result<(u64, u64)> {
422 let header_offset = info
423 .root_hier_offset
424 .checked_sub(60)
425 .ok_or_else(|| Error::InvalidData("COPC root hierarchy has no EVLR header".into()))?;
426 let mut header = [0u8; 60];
427 source.read_range(header_offset, &mut header)?;
428 let reserved = u16::from_le_bytes(header[0..2].try_into().expect("EVLR reserved width"));
429 let user_id = crate::trim_nul(&header[2..18]);
430 let record_id = u16::from_le_bytes(header[18..20].try_into().expect("EVLR record id width"));
431 let body_size = u64::from_le_bytes(header[20..28].try_into().expect("EVLR body size width"));
432 if reserved != 0 || user_id != "copc" || record_id != 1000 {
433 return Err(Error::InvalidData(
434 "COPC root hierarchy must immediately follow a copc/1000 EVLR header".into(),
435 ));
436 }
437 if info.root_hier_size > body_size {
438 return Err(Error::InvalidData(format!(
439 "COPC root hierarchy size {} exceeds hierarchy EVLR body size {body_size}",
440 info.root_hier_size
441 )));
442 }
443 crate::validate_range_in_file(
444 info.root_hier_offset,
445 body_size,
446 file_len,
447 "COPC hierarchy EVLR body",
448 )?;
449 Ok((info.root_hier_offset, body_size))
450}
451
452fn chunk_slice(group_bytes: &[u8], group_start: u64, entry: Entry) -> Result<(&[u8], usize)> {
453 let points_in_chunk = usize::try_from(entry.point_count).map_err(|_| {
454 Error::InvalidData(format!(
455 "negative point count {} for {:?}",
456 entry.point_count, entry.key
457 ))
458 })?;
459 let offset = usize::try_from(entry.offset - group_start)
460 .map_err(|_| Error::InvalidData("chunk offset exceeds usize".into()))?;
461 let byte_size = usize::try_from(entry.byte_size).map_err(|_| {
462 Error::InvalidData(format!(
463 "invalid byte size {} for {:?}",
464 entry.byte_size, entry.key
465 ))
466 })?;
467 let end = offset
468 .checked_add(byte_size)
469 .filter(|end| *end <= group_bytes.len())
470 .ok_or_else(|| Error::InvalidData("chunk range exceeds fetched group".into()))?;
471 Ok((&group_bytes[offset..end], points_in_chunk))
472}