1use std::sync::Arc;
8
9use super::*;
10use crate::dict::{ChunkLeaf, ChunkedSection, LeafedChunks, SectionMeta};
11use crate::dictionary::Dictionary;
12use crate::file::{decompress_bounded, ByteRange, FileError};
13use crate::index::{GraphIndex, LeafFetch, LeafedTiles, TileLeaf, NUM_PERMS};
14use crate::reader::RangeReader;
15
16const TILE_GAP: u64 = 4096;
18const DICT_GAP: u64 = 64 * 1024;
19
20pub struct V06File {
22 pub h: Header06,
24 pub root: Root06,
26 pub reader: Arc<dyn RangeReader + Send + Sync>,
28}
29
30fn read_coalesced(
32 r: &(dyn RangeReader + Send + Sync),
33 ranges: &[(u64, u64)],
34 gap: u64,
35) -> Option<Vec<Vec<u8>>> {
36 let rs: Vec<ByteRange> = ranges
37 .iter()
38 .map(|&(offset, len)| ByteRange { offset, len })
39 .collect();
40 crate::file::read_coalesced(r, &rs, gap)
41}
42
43impl V06File {
44 pub fn open(
47 reader: Arc<dyn RangeReader + Send + Sync>,
48 first: &[u8],
49 ) -> Result<Self, FileError> {
50 let h = Header06::from_bytes(first)?;
51 if h.file_len != reader.len() {
52 return Err(FileError::Container(
53 "0x06 file length does not match the header (truncated?)",
54 ));
55 }
56 let rs = HEADER_LEN;
57 let re = rs + h.root_len as usize;
58 let root_bytes = if re <= first.len() {
59 first[rs..re].to_vec()
60 } else {
61 reader.read_at(rs as u64, h.root_len as u64)?
62 };
63 if digest16(&root_bytes) != h.root_digest {
64 return Err(FileError::Container("0x06 root digest mismatch"));
65 }
66 let root = Root06::decode(&root_bytes, h.internal_codec)?;
67 for s in &root.sections {
68 let a = s.abs(&h)?;
69 let end = a.offset + a.len;
70 let ok = if kind::is_front(s.kind) {
71 a.offset >= h.front_base() && end <= h.data_offset
72 } else {
73 a.offset >= h.data_offset && end <= h.file_len - FOOTER.len() as u64
74 };
75 if !ok {
76 return Err(FileError::Container("0x06 section out of its region"));
77 }
78 }
79 Ok(V06File { h, root, reader })
80 }
81
82 pub fn bound(&self, k: u8) -> u64 {
85 section_bound(&self.root, k)
86 }
87
88 pub fn section(&self, k: u8) -> Option<ByteRange> {
90 self.root.section(k).and_then(|s| s.abs(&self.h).ok())
91 }
92
93 pub fn fetch(&self) -> LeafFetch {
96 let r = self.reader.clone();
97 Arc::new(move |ranges: &[(u64, u64)]| read_coalesced(&*r, ranges, 0))
98 }
99
100 pub fn card(&self) -> Result<Option<Vec<u8>>, FileError> {
102 let r = self.h.card_range();
103 if r.len == 0 {
104 return Ok(None);
105 }
106 let stored = self.reader.read_at(r.offset, r.len)?;
107 if digest16(&stored) != self.h.card_digest {
108 return Err(FileError::Container("0x06 card digest mismatch"));
109 }
110 Ok(Some(decompress_bounded(
111 self.h.internal_codec,
112 &stored,
113 CARD_MAX,
114 )?))
115 }
116
117 pub fn build_record(&self) -> Result<Option<Vec<u8>>, FileError> {
119 let r = self.h.build_range();
120 if r.len == 0 {
121 return Ok(None);
122 }
123 Ok(Some(self.reader.read_at(r.offset, r.len)?))
124 }
125
126 pub fn schema_section_bytes(&self) -> Option<Vec<u8>> {
129 let r = self.section(kind::SCHEMA)?;
130 let stored = self.reader.read_at(r.offset, r.len).ok()?;
131 let body =
132 decompress_bounded(self.h.internal_codec, &stored, self.bound(kind::SCHEMA)).ok()?;
133 let mut out = digest16(&body).to_vec();
134 out.extend_from_slice(&body);
135 Some(out)
136 }
137
138 pub fn schema(&self) -> Option<crate::schema_section::SchemaMeta> {
140 crate::schema_section::SchemaMeta::decode(&self.schema_section_bytes()?).ok()
141 }
142
143 pub fn pyramid_bytes(&self) -> Option<Vec<u8>> {
145 let r = self.section(kind::PYRAMID)?;
146 self.reader.read_at(r.offset, r.len).ok()
147 }
148
149 pub fn dictionary(&self, want: [bool; 4]) -> Result<Dictionary, FileError> {
152 let unit = self
153 .root
154 .unit(tag::DICT)
155 .ok_or(FileError::Container("0x06 file has no dictionary"))?;
156 let roots = decode_dict_root(unit)?;
157 let leaves_base = self.section(kind::DICT_LEAVES).map_or(0, |r| r.offset);
158 let data_base = self.section(kind::DICT_DATA).map_or(0, |r| r.offset);
159 let cache = crate::chunk_cache::ChunkCache::unlimited_arc();
160 let codec = self.h.dict_codec;
161 let max = self.bound(kind::DICT_DATA);
162 let mut dirs: Vec<Option<Arc<LeafedChunks>>> = Vec::with_capacity(4);
163 let mut sections = Vec::with_capacity(4);
164 for (si, s) in roots.iter().enumerate() {
165 let meta = SectionMeta {
166 term_count: if want[si] { s.term_count } else { 0 },
167 restart_interval: s.restart_interval.max(1),
168 restart_offsets: Vec::new(),
169 };
170 if !want[si] {
171 sections.push(ChunkedSection::from_parts(
172 meta,
173 Vec::new(),
174 None,
175 cache.clone(),
176 si as u8,
177 ));
178 dirs.push(None);
179 continue;
180 }
181 let leaves: Vec<ChunkLeaf> = s
182 .leaves
183 .iter()
184 .map(|(l, run, coff, key)| ChunkLeaf {
185 first: l.first,
186 n: l.n,
187 first_run: *run,
188 key: key.clone(),
189 leaf: (leaves_base + l.off, l.len),
190 chunk_base: data_base + s.data_off + coff,
191 })
192 .collect();
193 let dir = Arc::new(
194 LeafedChunks::new(leaves, s.n_chunks, self.fetch(), self.h.internal_codec)
195 .with_max_block(self.bound(kind::DICT_LEAVES)),
196 );
197 let (r, d) = (self.reader.clone(), dir.clone());
198 let loader: crate::dict::ChunkLoader = Box::new(move |ci| {
199 let (off, len) = d.range(ci)?;
200 let b = r.read_at(off, len).ok()?;
201 decompress_bounded(codec, &b, max).ok()
202 });
203 let (r, d) = (self.reader.clone(), dir.clone());
204 let bulk: crate::dict::ChunkBulkLoader = Box::new(move |cis| {
205 d.ensure_leaves(&d.leaves_of(cis));
206 let ranges: Option<Vec<(u64, u64)>> = cis.iter().map(|&ci| d.range(ci)).collect();
207 let blobs = read_coalesced(&*r, &ranges?, DICT_GAP)?;
208 blobs
209 .iter()
210 .map(|b| decompress_bounded(codec, b, max).ok())
211 .collect()
212 });
213 sections.push(
214 ChunkedSection::leafed(meta, dir.clone(), loader, cache.clone(), si as u8)
215 .with_bulk_loader(bulk),
216 );
217 dirs.push(Some(dir));
218 }
219 let arr: [ChunkedSection; 4] = sections
220 .try_into()
221 .map_err(|_| FileError::Container("expected 4 dictionary sections"))?;
222 let r = self.reader.clone();
223 let multi: crate::dict::MultiChunkBulkLoader = Box::new(move |pairs| {
224 let mut per: [Vec<usize>; 4] = Default::default();
226 for &(si, ci) in pairs {
227 if let Some(v) = per.get_mut(si as usize) {
228 v.push(ci);
229 }
230 }
231 let mut leaf_ranges: Vec<(u64, u64)> = Vec::new();
232 let _ = &mut leaf_ranges;
233 for (si, cis) in per.iter().enumerate() {
234 if let Some(Some(d)) = dirs.get(si) {
235 d.ensure_leaves(&d.leaves_of(cis));
236 }
237 }
238 let ranges: Option<Vec<(u64, u64)>> = pairs
239 .iter()
240 .map(|&(si, ci)| dirs.get(si as usize)?.as_ref()?.range(ci))
241 .collect();
242 let blobs = read_coalesced(&*r, &ranges?, DICT_GAP)?;
243 blobs
244 .iter()
245 .map(|b| decompress_bounded(codec, b, max).ok())
246 .collect()
247 });
248 let mut dict = Dictionary::from_chunked_sections(arr).with_multi_bulk_loader(multi);
249 dict.set_has_quoted_triples(self.h.flags & F_QUOTED != 0);
250 Ok(dict)
251 }
252
253 pub fn index(&self, read_concurrency: usize) -> Result<GraphIndex, FileError> {
255 let unit = self
256 .root
257 .unit(tag::INDEX)
258 .ok_or(FileError::Container("0x06 file has no index"))?;
259 let perms_root = decode_index_root(unit)?;
260 let leaves_base = self.section(kind::INDEX_LEAVES).map_or(0, |r| r.offset);
261 let data_base = self.section(kind::INDEX_DATA).map_or(0, |r| r.offset);
262 let perms = self.h.perms();
263 let mut lists: [Option<Arc<LeafedTiles>>; NUM_PERMS] = Default::default();
264 for p in &perms_root {
265 if p.si >= NUM_PERMS {
266 return Err(FileError::Container("0x06 index: bad permutation"));
267 }
268 let leaves: Vec<TileLeaf> = p
269 .leaves
270 .iter()
271 .map(|(l, mn, mx, toff)| TileLeaf {
272 first: l.first,
273 n: l.n,
274 min_a: *mn,
275 max_a: *mx,
276 leaf: (leaves_base + l.off, l.len),
277 tile_base: data_base + p.data_off + toff,
278 })
279 .collect();
280 lists[p.si] = Some(Arc::new(
281 LeafedTiles::new(
282 leaves,
283 p.n_tiles,
284 p.data_len,
285 self.fetch(),
286 self.h.internal_codec,
287 )
288 .with_max_block(self.bound(kind::INDEX_LEAVES)),
289 ));
290 }
291 for perm in perms.iter() {
292 if lists[perm.section_index()].is_none() {
293 return Err(FileError::Container(
294 "0x06 index lacks a stored permutation",
295 ));
296 }
297 }
298 let codec = self.h.block_codec;
299 let max = self.bound(kind::INDEX_DATA);
300 let (r, l) = (self.reader.clone(), lists.clone());
301 let loader: crate::index::TileLoader = Box::new(move |si, ti| {
302 let (off, len) = l.get(si)?.as_ref()?.range(ti)?;
303 let b = r.read_at(off, len).ok()?;
304 decompress_bounded(codec, &b, max).ok()
305 });
306 let (r, l) = (self.reader.clone(), lists.clone());
307 let bulk: crate::index::TileBulkLoader = Box::new(move |si, tis| {
308 let list = l.get(si)?.as_ref()?;
309 if let (Some(&a), Some(&b)) = (tis.first(), tis.last()) {
310 list.ensure_span(a, b + 1);
311 }
312 let ranges: Option<Vec<(u64, u64)>> = tis.iter().map(|&ti| list.range(ti)).collect();
313 let blobs = read_coalesced(&*r, &ranges?, TILE_GAP)?;
314 blobs
315 .iter()
316 .map(|b| decompress_bounded(codec, b, max).ok())
317 .collect()
318 });
319 let mut index = GraphIndex::from_leafed(lists, perms, loader).with_bulk_loader(bulk);
320 index.set_read_concurrency(read_concurrency);
321 Ok(index)
322 }
323
324 pub fn text_index(&self) -> Option<Result<crate::text_index::TextIndex, FileError>> {
326 let unit = self.root.unit(tag::TEXT)?;
327 Some((|| {
328 let t = decode_text_root(unit)?;
329 let leaves_base = self.section(kind::TEXT_LEAVES).map_or(0, |r| r.offset);
330 let data = self
331 .section(kind::TEXT_DATA)
332 .unwrap_or(ByteRange { offset: 0, len: 0 });
333 let leaves = t
334 .leaves
335 .iter()
336 .map(|(l, tok)| crate::text_index::TokenLeaf {
337 first: l.first,
338 n: l.n,
339 first_token: String::from_utf8_lossy(tok).into_owned(),
340 leaf: (leaves_base + l.off, l.len),
341 })
342 .collect();
343 let tokens = Arc::new(
344 crate::text_index::LeafedTokens::new(
345 leaves,
346 t.n_tokens,
347 self.fetch(),
348 self.h.internal_codec,
349 )
350 .with_max_block(self.bound(kind::TEXT_LEAVES)),
351 );
352 let r = self.reader.clone();
353 let loader = Box::new(move |off: u64, len: u64| {
354 if off.checked_add(len)? > data.len {
355 return None;
356 }
357 r.read_at(data.offset + off, len).ok()
358 });
359 crate::text_index::TextIndex::leafed(tokens, &t.scope, loader)
360 })())
361 }
362
363 pub fn token_table_bytes(&self) -> Option<u64> {
366 self.root.unit(tag::TEXT)?;
367 Some(self.section(kind::TEXT_LEAVES).map_or(0, |r| r.len))
368 }
369
370 pub fn label_index(&self) -> Option<Result<crate::label_index::LabelIndex, FileError>> {
372 let unit = self.root.unit(tag::LABELS)?;
373 Some((|| {
374 let l = decode_label_root(unit)?;
375 let data = self
376 .section(kind::LABEL_DATA)
377 .unwrap_or(ByteRange { offset: 0, len: 0 });
378 let nodes = self
379 .section(kind::LABEL_LEAVES)
380 .unwrap_or(ByteRange { offset: 0, len: 0 });
381 if data.len != l.data_len {
382 return Err(FileError::Container("0x06 label data length mismatch"));
383 }
384 let head_len = l.head.len() as u64;
385 let section_len = head_len + data.len + nodes.len;
386 let r = self.reader.clone();
387 let head = l.head.clone();
388 let loader = Box::new(move |off: u64, len: u64| -> Option<Vec<u8>> {
389 let end = off.checked_add(len)?;
390 if end <= head_len {
391 return head.get(off as usize..end as usize).map(<[u8]>::to_vec);
392 }
393 let bo = off.checked_sub(head_len)?;
394 if bo + len <= data.len {
395 r.read_at(data.offset + bo, len).ok()
396 } else if bo >= data.len && bo - data.len + len <= nodes.len {
397 r.read_at(nodes.offset + (bo - data.len), len).ok()
398 } else {
399 None
400 }
401 });
402 let max = self
403 .bound(kind::LABEL_LEAVES)
404 .max(self.bound(kind::LABEL_DATA));
405 crate::label_index::LabelIndex::from_head(
406 &l.head,
407 section_len,
408 self.h.block_codec,
409 loader,
410 )
411 .map(|li| li.with_max_block(max))
412 })())
413 }
414
415 pub fn label_entry_count(&self) -> Option<u64> {
417 let l = decode_label_root(self.root.unit(tag::LABELS)?).ok()?;
418 crate::label_index::LabelIndexPreamble::parse(&l.head)
419 .ok()
420 .map(|p| p.entry_count)
421 }
422
423 pub fn quad_root(&self) -> Option<Result<QuadRoot, FileError>> {
425 self.root.unit(tag::QUAD).map(decode_quad_root)
426 }
427
428 pub(crate) fn quad(
430 &self,
431 read_concurrency: usize,
432 ) -> Option<Result<crate::quad::QuadNamed, FileError>> {
433 let root = match self.quad_root()? {
434 Ok(r) => r,
435 Err(e) => return Some(Err(e)),
436 };
437 let leaves = self.section(kind::QUAD_LEAVES).map_or(0, |r| r.offset);
438 let data = self.section(kind::QUAD_DATA).map_or(0, |r| r.offset);
439 Some(
440 crate::quad::V06Quad::new(&root, leaves, data, self.h.internal_codec).map(|mut v| {
441 v.tile_max = self.bound(kind::QUAD_DATA);
442 v.leaf_max = self.bound(kind::QUAD_LEAVES);
443 crate::quad::QuadNamed::new_v06(
444 self.reader.clone(),
445 v,
446 self.h.block_codec,
447 self.h.perms(),
448 read_concurrency,
449 )
450 }),
451 )
452 }
453
454 pub fn verify(&self, only: Option<u8>) -> Result<bool, FileError> {
458 Ok(self.verify_report(only)?.is_empty())
459 }
460
461 pub fn verify_report(&self, only: Option<u8>) -> Result<Vec<String>, FileError> {
463 const SLICE: u64 = 8 << 20;
464 let mut bad = Vec::new();
465 if content_hash(&self.h) != self.h.content_hash {
466 bad.push("content hash".to_string());
467 }
468 let card = self.h.card_range();
469 if only.is_none() && card.len > 0 {
470 let stored = self.reader.read_at(card.offset, card.len)?;
471 if digest16(&stored) != self.h.card_digest {
472 bad.push("card".to_string());
473 }
474 }
475 for s in &self.root.sections {
476 if only.is_some_and(|k| k != s.kind) {
477 continue;
478 }
479 let a = s.abs(&self.h)?;
480 let mut h = blake3::Hasher::new();
481 let mut at = a.offset;
482 while at < a.offset + a.len {
483 let n = (a.offset + a.len - at).min(SLICE);
484 h.update(&self.reader.read_at(at, n)?);
485 at += n;
486 }
487 if h.finalize().as_bytes()[..16] != s.digest {
488 bad.push(kind::name(s.kind).to_string());
489 }
490 }
491 if only.is_none() {
492 let foot = self
493 .reader
494 .read_at(self.h.file_len - FOOTER.len() as u64, FOOTER.len() as u64)?;
495 if foot != FOOTER {
496 bad.push("footer".to_string());
497 }
498 }
499 Ok(bad)
500 }
501}
502
503pub fn is_v06(first: &[u8]) -> bool {
505 first.len() > 4 && first[0..4] == crate::header::MAGIC && first[4] == VERSION
506}
507
508pub fn read_front<R: RangeReader + ?Sized>(r: &R) -> Result<(Header06, Root06), FileError> {
511 let head = r.read_at(0, HEADER_LEN as u64)?;
512 let h = Header06::from_bytes(&head)?;
513 let root_bytes = r.read_at(HEADER_LEN as u64, h.root_len as u64)?;
514 if digest16(&root_bytes) != h.root_digest {
515 return Err(FileError::Container("0x06 root digest mismatch"));
516 }
517 let root = Root06::decode(&root_bytes, h.internal_codec)?;
518 Ok((h, root))
519}
520
521pub fn read_section<R: RangeReader + ?Sized>(
523 r: &R,
524 h: &Header06,
525 root: &Root06,
526 k: u8,
527) -> Result<Option<Vec<u8>>, FileError> {
528 match root.section(k) {
529 Some(s) => {
530 let a = s.abs(h)?;
531 Ok(Some(r.read_at(a.offset, a.len)?))
532 }
533 None => Ok(None),
534 }
535}
536
537#[allow(clippy::type_complexity)]
540pub fn read_card_build<R: RangeReader + ?Sized>(
541 r: &R,
542 h: &Header06,
543) -> Result<(Option<Vec<u8>>, Option<Vec<u8>>), FileError> {
544 let c = h.card_range();
545 let b = h.build_range();
546 if c.len == 0 && b.len == 0 {
547 return Ok((None, None));
548 }
549 let lo = if c.len == 0 {
550 b.offset
551 } else if b.len == 0 {
552 c.offset
553 } else {
554 c.offset.min(b.offset)
555 };
556 let hi = (c.offset + c.len).max(b.offset + b.len);
557 let span = r.read_at(lo, hi - lo)?;
558 let card = if c.len > 0 {
559 let stored = &span[(c.offset - lo) as usize..(c.offset - lo + c.len) as usize];
560 if digest16(stored) != h.card_digest {
561 return Err(FileError::Container("0x06 card digest mismatch"));
562 }
563 Some(decompress_bounded(h.internal_codec, stored, CARD_MAX)?)
564 } else {
565 None
566 };
567 let build = (b.len > 0)
568 .then(|| span[(b.offset - lo) as usize..(b.offset - lo + b.len) as usize].to_vec());
569 Ok((card, build))
570}
571
572pub fn section_bound(root: &Root06, k: u8) -> u64 {
575 root.section(k)
576 .map(|s| s.max_block as u64)
577 .filter(|&m| m > 0)
578 .unwrap_or(BLOCK_CEILING)
579}
580
581pub fn read_schema_bytes<R: RangeReader + ?Sized>(r: &R) -> Result<Option<Vec<u8>>, FileError> {
583 let (h, root) = read_front(r)?;
584 let Some(stored) = read_section(r, &h, &root, kind::SCHEMA)? else {
585 return Ok(None);
586 };
587 let body = decompress_bounded(
588 h.internal_codec,
589 &stored,
590 section_bound(&root, kind::SCHEMA),
591 )?;
592 let mut out = digest16(&body).to_vec();
593 out.extend_from_slice(&body);
594 Ok(Some(out))
595}
596
597pub fn read_pyramid<R: RangeReader + ?Sized>(r: &R) -> Result<Option<Vec<u8>>, FileError> {
599 let (h, root) = read_front(r)?;
600 read_section(r, &h, &root, kind::PYRAMID)
601}
602
603pub fn read_token_table_bytes<R: RangeReader + ?Sized>(r: &R) -> Option<u64> {
605 let (_, root) = read_front(r).ok()?;
606 root.unit(tag::TEXT)?;
607 Some(root.section(kind::TEXT_LEAVES).map_or(0, |s| s.len))
608}
609
610pub struct SparseReader {
613 len: u64,
614 parts: Vec<(u64, Vec<u8>)>,
615}
616
617impl SparseReader {
618 pub fn fetch<R: RangeReader + ?Sized>(
621 r: &R,
622 first: Vec<u8>,
623 ranges: &[ByteRange],
624 ) -> Result<Self, FileError> {
625 let held = first.len() as u64;
626 let want: Vec<(u64, u64)> = ranges
627 .iter()
628 .filter(|g| g.len > 0 && g.offset.saturating_add(g.len) > held)
629 .map(|g| (g.offset, g.len))
630 .collect();
631 let mut parts = vec![(0u64, first)];
632 if !want.is_empty() {
633 for ((o, _), b) in want.iter().zip(r.read_many(&want)?) {
634 parts.push((*o, b));
635 }
636 }
637 Ok(SparseReader {
638 len: r.len(),
639 parts,
640 })
641 }
642}
643
644pub struct Prefixed<'a, R: RangeReader + ?Sized> {
647 pub first: &'a [u8],
649 pub r: &'a R,
651}
652
653impl<R: RangeReader + ?Sized> RangeReader for Prefixed<'_, R> {
654 fn len(&self) -> u64 {
655 self.r.len()
656 }
657
658 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
659 match offset.checked_add(len) {
660 Some(end) if end <= self.first.len() as u64 => {
661 Ok(self.first[offset as usize..end as usize].to_vec())
662 }
663 _ => self.r.read_at(offset, len),
664 }
665 }
666}
667
668impl RangeReader for SparseReader {
669 fn len(&self) -> u64 {
670 self.len
671 }
672
673 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
674 for (o, b) in &self.parts {
675 if offset >= *o && offset + len <= o + b.len() as u64 {
676 let s = (offset - o) as usize;
677 return Ok(b[s..s + len as usize].to_vec());
678 }
679 }
680 Err(std::io::Error::other("0x06: range not held by this reader"))
681 }
682}
683
684#[allow(clippy::type_complexity)]
687pub fn read_signals<R: RangeReader + ?Sized>(
688 r: &R,
689) -> Result<(Option<(u64, u64)>, Option<(u64, Option<u64>)>), FileError> {
690 let (_, root) = read_front(r)?;
691 let len = |k: u8| root.section(k).map_or(0, |s| s.len);
692 let text = root.unit(tag::TEXT).map(|_| {
693 (
694 len(kind::TEXT_LEAVES) + len(kind::TEXT_DATA),
695 len(kind::TEXT_LEAVES),
696 )
697 });
698 let labels = match root.unit(tag::LABELS) {
699 None => None,
700 Some(u) => {
701 let l = decode_label_root(u)?;
702 let entries = crate::label_index::LabelIndexPreamble::parse(&l.head)
703 .ok()
704 .map(|p| p.entry_count);
705 Some((
706 l.head.len() as u64 + len(kind::LABEL_DATA) + len(kind::LABEL_LEAVES),
707 entries,
708 ))
709 }
710 };
711 Ok((text, labels))
712}