md_codec/chunk.rs
1//! Chunk header per SPEC v0.30 §2.2.
2//!
3//! Encodes the 37-bit chunked wire-format header. First-symbol layout
4//! MSB-first: `[v3][v2][v1][v0][chunked]` (4-bit version + 1-bit chunked-flag).
5//! Remainder: 20-bit chunk-set-id + 6-bit count-minus-1 + 6-bit index.
6//! Total = 4 + 1 + 20 + 6 + 6 = 37 bits.
7//!
8//! v0.34.0: also hosts [`decode_with_correction`] — the BCH-error-correcting
9//! decode entry point. Per chunk: parse → polymod-residue → (if non-zero)
10//! call [`crate::bch_decode::decode_regular_errors`] → apply corrections →
11//! re-encode → forward to [`reassemble`]. Atomic per plan §1 D28: any chunk
12//! exceeding the BCH `t = 4` capacity fails the whole call without partial
13//! output.
14
15use crate::bitstream::{BitReader, BitWriter};
16use crate::error::Error;
17use crate::header::Header;
18
19/// Wire header for a single chunk in a chunked v0.30 payload.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ChunkHeader {
22 /// Wire-format version (4 bits). v0.30 = 4.
23 pub version: u8,
24 /// 20-bit chunk-set identifier shared by all chunks in a set.
25 pub chunk_set_id: u32,
26 /// Total number of chunks in the set; valid range `1..=64`.
27 pub count: u8,
28 /// Zero-based index of this chunk within the set; must be `< count`.
29 pub index: u8,
30}
31
32impl ChunkHeader {
33 /// Encode the chunk header into `w` as 37 bits.
34 ///
35 /// Returns an error if `count`, `index`, or `chunk_set_id` are out of range.
36 pub fn write(&self, w: &mut BitWriter) -> Result<(), Error> {
37 if !(1..=64).contains(&(self.count as u32)) {
38 return Err(Error::ChunkCountOutOfRange { count: self.count });
39 }
40 if self.index >= self.count {
41 return Err(Error::ChunkIndexOutOfRange {
42 index: self.index,
43 count: self.count,
44 });
45 }
46 if self.chunk_set_id >= (1 << 20) {
47 return Err(Error::ChunkSetIdOutOfRange {
48 id: self.chunk_set_id,
49 });
50 }
51 w.write_bits(u64::from(self.version & 0b1111), 4);
52 w.write_bits(1, 1); // chunked = 1
53 w.write_bits(u64::from(self.chunk_set_id), 20);
54 w.write_bits((self.count - 1) as u64, 6); // count-1 offset
55 w.write_bits(u64::from(self.index), 6);
56 Ok(())
57 }
58
59 /// Decode a chunk header (37 bits) from `r`.
60 ///
61 /// Returns [`Error::WireVersionMismatch`] if the 4-bit version field
62 /// is not `WF_REDESIGN_VERSION` per SPEC §2.5 (e.g., v0.x chunked
63 /// payloads where version=0 in the first 3 wire bits become version=0
64 /// or version=1 under the v0.30 4-bit read depending on prior bits).
65 /// Returns [`Error::ChunkHeaderChunkedFlagMissing`] if the chunked-flag
66 /// bit is not set after the version check passes.
67 pub fn read(r: &mut BitReader) -> Result<Self, Error> {
68 let version = r.read_bits(4)? as u8;
69 if version != Header::WF_REDESIGN_VERSION {
70 return Err(Error::WireVersionMismatch { got: version });
71 }
72 let chunked = r.read_bits(1)? != 0;
73 if !chunked {
74 return Err(Error::ChunkHeaderChunkedFlagMissing);
75 }
76 let chunk_set_id = r.read_bits(20)? as u32;
77 let count = (r.read_bits(6)? + 1) as u8;
78 let index = r.read_bits(6)? as u8;
79 Ok(Self {
80 version,
81 chunk_set_id,
82 count,
83 index,
84 })
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::header::Header;
92
93 #[test]
94 fn chunk_header_round_trip() {
95 let h = ChunkHeader {
96 version: Header::WF_REDESIGN_VERSION,
97 chunk_set_id: 0xABCDE,
98 count: 3,
99 index: 1,
100 };
101 let mut w = BitWriter::new();
102 h.write(&mut w).unwrap();
103 // 4 + 1 + 20 + 6 + 6 = 37 bits
104 assert_eq!(w.bit_len(), 37);
105 let bytes = w.into_bytes();
106 let mut r = BitReader::new(&bytes);
107 assert_eq!(ChunkHeader::read(&mut r).unwrap(), h);
108 }
109
110 #[test]
111 fn chunk_header_count_64_round_trip() {
112 let h = ChunkHeader {
113 version: Header::WF_REDESIGN_VERSION,
114 chunk_set_id: 0,
115 count: 64,
116 index: 63,
117 };
118 let mut w = BitWriter::new();
119 h.write(&mut w).unwrap();
120 let bytes = w.into_bytes();
121 let mut r = BitReader::new(&bytes);
122 assert_eq!(ChunkHeader::read(&mut r).unwrap(), h);
123 }
124
125 #[test]
126 fn chunk_header_count_zero_rejected() {
127 let h = ChunkHeader {
128 version: Header::WF_REDESIGN_VERSION,
129 chunk_set_id: 0,
130 count: 0,
131 index: 0,
132 };
133 let mut w = BitWriter::new();
134 assert!(matches!(
135 h.write(&mut w),
136 Err(Error::ChunkCountOutOfRange { count: 0 })
137 ));
138 }
139
140 /// SPEC v0.30 §2.5 v0.x rejection for chunk-header path. A wire crafted
141 /// with version=0 and chunked-flag=1 (the v0.30-layout interpretation of
142 /// what a v0.x chunked first-symbol becomes when reordered) must be
143 /// rejected with `WireVersionMismatch { got: 0 }`.
144 #[test]
145 fn chunk_header_rejects_v0x_version() {
146 // Construct first 5 bits MSB-first: [v3=0][v2=0][v1=0][v0=0][chunked=1]
147 // = 0b00001 (numeric 1)
148 // Pad with 32 zero bits (chunk_set_id + count-1 + index) to reach
149 // the full 37-bit chunk header length. 37 bits packed MSB-first into
150 // 5 bytes (with 3 trailing zero bits beyond the bit limit).
151 // Easier: use BitWriter to build the wire deterministically.
152 let mut w = BitWriter::new();
153 w.write_bits(0, 4); // version = 0 (v0.x)
154 w.write_bits(1, 1); // chunked = 1
155 w.write_bits(0, 20); // chunk_set_id
156 w.write_bits(0, 6); // count-1
157 w.write_bits(0, 6); // index
158 assert_eq!(w.bit_len(), 37);
159 let bytes = w.into_bytes();
160 let mut r = BitReader::new(&bytes);
161 assert!(matches!(
162 ChunkHeader::read(&mut r),
163 Err(Error::WireVersionMismatch { got: 0 })
164 ));
165 }
166}
167
168use crate::identity::Md1EncodingId;
169
170/// Derive the 20-bit chunk-set-id from a [`Md1EncodingId`] by taking the
171/// top 20 bits of the underlying 16-byte hash, MSB-first.
172///
173/// The chunk-set-id groups chunks belonging to the same encoded payload.
174/// Returned value is in the range `0..=0xFFFFF`.
175pub fn derive_chunk_set_id(id: &Md1EncodingId) -> u32 {
176 // First 20 bits of Md1EncodingId[0..16], MSB-first.
177 let bytes = id.as_bytes();
178 ((bytes[0] as u32) << 12) | ((bytes[1] as u32) << 4) | ((bytes[2] as u32) >> 4)
179}
180
181#[cfg(test)]
182mod chunk_set_id_tests {
183 use super::*;
184
185 #[test]
186 fn derive_chunk_set_id_deterministic() {
187 let mut bytes = [0u8; 16];
188 bytes[0] = 0xab;
189 bytes[1] = 0xcd;
190 bytes[2] = 0xe1;
191 bytes[3] = 0x23;
192 let id = Md1EncodingId::new(bytes);
193 let csid_a = derive_chunk_set_id(&id);
194 let csid_b = derive_chunk_set_id(&id);
195 assert_eq!(csid_a, csid_b);
196 }
197
198 #[test]
199 fn derive_chunk_set_id_msb_first_extraction() {
200 // bytes[0]=0xAB, [1]=0xCD, [2]=0xEF: top 20 bits = 0xABCDE
201 let mut bytes = [0u8; 16];
202 bytes[0] = 0xAB;
203 bytes[1] = 0xCD;
204 bytes[2] = 0xEF;
205 let id = Md1EncodingId::new(bytes);
206 assert_eq!(derive_chunk_set_id(&id), 0xABCDE);
207 }
208}
209
210use crate::encode::Descriptor;
211
212/// Threshold (in payload bits) above which chunking is required. Derived from
213/// codex32 *regular*-form's 80-char data-part limit (per BIP 93): 3 HRP + 1
214/// separator + 64 data + 13 checksum (see `codex32::REGULAR_CHECKSUM_SYMBOLS`).
215/// Long-form codex32 was dropped in v0.12.0, so the legal data-symbol budget
216/// per chunk is 64 = 320 bits.
217/// Encoders attempt single-string emit first; if the codex32 wrapping reports
218/// "too long", split into N chunks.
219pub const SINGLE_STRING_PAYLOAD_BIT_LIMIT: usize = 64 * 5;
220
221/// Split a [`Descriptor`] into N codex32 md1 strings, each carrying a chunk
222/// header and a slice of the canonical payload.
223///
224/// Algorithm:
225/// 1. Encode the full payload (`encode_payload`).
226/// 2. Compute [`crate::identity::Md1EncodingId`]; derive `ChunkSetId`.
227/// 3. Choose chunk count N such that each chunk fits in codex32 long form
228/// after adding the 37-bit chunk header.
229/// 4. Split the payload into N approximately-equal byte-boundary slices.
230/// 5. For each chunk i: prepend chunk header (37 bits), wrap via codex32 with
231/// the chunked-flag bit set, emit md1 string.
232///
233/// Note: `bytes_per_chunk` could be 0 if `payload_bytes` were empty, but the
234/// encoder validates `n ≥ 1` so the payload is always non-empty.
235pub fn split(d: &Descriptor) -> Result<Vec<String>, Error> {
236 use crate::bitstream::BitWriter;
237 use crate::encode::encode_payload;
238 use crate::identity::compute_md1_encoding_id;
239
240 let (payload_bytes, _payload_bits) = encode_payload(d)?;
241
242 // Compute ChunkSetId from full-encoding hash.
243 let md1_id = compute_md1_encoding_id(d)?;
244 let chunk_set_id = derive_chunk_set_id(&md1_id);
245
246 // Choose chunk count from payload byte count (≤7 bits of trailing
247 // codex32-padding are tolerated by the reassembled-stream TLV-rollback).
248 let payload_bit_count_for_sizing = payload_bytes.len() * 8;
249 let chunks_needed = payload_bit_count_for_sizing.div_ceil(SINGLE_STRING_PAYLOAD_BIT_LIMIT);
250 if chunks_needed > 64 {
251 return Err(Error::ChunkCountExceedsMax {
252 needed: chunks_needed,
253 });
254 }
255 let count: u8 = if chunks_needed == 0 {
256 1
257 } else {
258 chunks_needed as u8
259 };
260
261 // Split payload into `count` byte-boundary slices.
262 let bytes_per_chunk = payload_bytes.len().div_ceil(count as usize);
263
264 let mut chunks = Vec::with_capacity(count as usize);
265 for index in 0..count {
266 let start_byte = (index as usize) * bytes_per_chunk;
267 let end_byte = ((index as usize + 1) * bytes_per_chunk).min(payload_bytes.len());
268 let chunk_payload_bytes = &payload_bytes[start_byte..end_byte];
269
270 // Build per-chunk wire: 37-bit chunk header + chunk-payload bytes
271 // (full 8 bits per byte, no further fractional content). Chunk's
272 // exact bit count = 37 + 8 × |chunk_payload_bytes|.
273 let header = ChunkHeader {
274 version: Header::WF_REDESIGN_VERSION,
275 chunk_set_id,
276 count,
277 index,
278 };
279 let mut w = BitWriter::new();
280 header.write(&mut w)?;
281 for byte in chunk_payload_bytes {
282 w.write_bits(u64::from(*byte), 8);
283 }
284 let chunk_bit_count = 37 + 8 * chunk_payload_bytes.len();
285 let bytes = w.into_bytes();
286 let s = crate::codex32::wrap_payload(&bytes, chunk_bit_count)?;
287 chunks.push(s);
288 }
289 Ok(chunks)
290}
291
292use crate::decode::decode_payload;
293
294/// Reassemble a [`Descriptor`] from N md1 codex32 strings.
295///
296/// Algorithm:
297/// 1. Unwrap each string via the codex32 layer (verifies BCH per chunk).
298/// 2. Parse the 37-bit chunk header from each.
299/// 3. Validate consistency: same version, chunk_set_id, count.
300/// 4. Sort by index; verify `0..count-1` with no gaps.
301/// 5. Concatenate per-chunk payload bytes.
302/// 6. Decode the reassembled payload via [`decode_payload`].
303/// 7. Verify the reassembled payload's derived chunk-set-id matches the
304/// chunk-set-id present in every chunk header (cross-chunk integrity).
305pub fn reassemble(strings: &[&str]) -> Result<Descriptor, Error> {
306 use crate::bitstream::BitReader;
307 use crate::codex32::unwrap_string;
308 use crate::identity::compute_md1_encoding_id;
309
310 if strings.is_empty() {
311 return Err(Error::ChunkSetEmpty);
312 }
313
314 // Unwrap each, parse 37-bit chunk header, then read whole payload bytes.
315 // Use the symbol-aligned bit count returned by `unwrap_string` (NOT
316 // `bytes.len() * 8`, which would over-estimate by up to 7 bits and break
317 // round-trip for chunks where symbol-padding plus byte-padding crosses a
318 // byte boundary — e.g. N=3, N=8, etc.).
319 let mut parsed: Vec<(ChunkHeader, Vec<u8>)> = Vec::with_capacity(strings.len());
320 for s in strings {
321 let (bytes, symbol_aligned_bit_count) = unwrap_string(s)?;
322 let mut r = BitReader::with_bit_limit(&bytes, symbol_aligned_bit_count);
323 let header = ChunkHeader::read(&mut r)?;
324 // Per encoder contract: chunk wire is exactly 37 + 8N bits. The
325 // symbol-aligned bit count is `ceil((37+8N)/5) * 5`, which is in
326 // [37+8N, 37+8N+4]. So `(symbol_aligned_bit_count - 37) / 8`
327 // (floor) recovers exactly N.
328 let payload_byte_count = (symbol_aligned_bit_count - 37) / 8;
329 let mut chunk_payload_bytes = Vec::with_capacity(payload_byte_count);
330 for _ in 0..payload_byte_count {
331 let v = r.read_bits(8)? as u8;
332 chunk_payload_bytes.push(v);
333 }
334 // Trailing ≤4 symbol-padding bits remain in r; discard.
335 parsed.push((header, chunk_payload_bytes));
336 }
337
338 // Validate consistency.
339 let (h0, _) = &parsed[0];
340 let expected_count = h0.count;
341 let expected_csid = h0.chunk_set_id;
342 let expected_version = h0.version;
343 for (h, _) in &parsed {
344 if h.count != expected_count
345 || h.chunk_set_id != expected_csid
346 || h.version != expected_version
347 {
348 return Err(Error::ChunkSetInconsistent);
349 }
350 }
351 if parsed.len() != expected_count as usize {
352 return Err(Error::ChunkSetIncomplete {
353 got: parsed.len(),
354 expected: expected_count as usize,
355 });
356 }
357
358 // Sort by index; verify 0..count-1 with no gaps.
359 parsed.sort_by_key(|(h, _)| h.index);
360 for (i, (h, _)) in parsed.iter().enumerate() {
361 if h.index as usize != i {
362 return Err(Error::ChunkIndexGap {
363 expected: i as u8,
364 got: h.index,
365 });
366 }
367 }
368
369 // Concatenate chunk payload bytes.
370 let mut full_bytes = Vec::new();
371 for (_, chunk_bytes) in &parsed {
372 full_bytes.extend_from_slice(chunk_bytes);
373 }
374
375 // Decode payload. bit_len = bytes.len() * 8; TLV-rollback handles trailing padding.
376 let descriptor = decode_payload(&full_bytes, full_bytes.len() * 8)?;
377
378 // Cross-chunk integrity check.
379 let md1_id = compute_md1_encoding_id(&descriptor)?;
380 let derived_csid = derive_chunk_set_id(&md1_id);
381 if derived_csid != expected_csid {
382 return Err(Error::ChunkSetIdMismatch {
383 expected: expected_csid,
384 derived: derived_csid,
385 });
386 }
387
388 Ok(descriptor)
389}
390
391// ---------------------------------------------------------------------------
392// v0.34.0: BCH-error-correcting decode (plan §1 D22 + §2.B.1).
393// ---------------------------------------------------------------------------
394
395/// Per-correction report emitted by [`decode_with_correction`]. One entry
396/// per repaired character. `position` is 0-indexed into the codex32
397/// data-part (i.e. the characters following the `md1` HRP + separator);
398/// `was` is the original (corrupted) char from the input; `now` is the
399/// corrected char.
400///
401/// Atomic per plan §1 D28: when [`decode_with_correction`] succeeds the
402/// returned vector aggregates corrections across all chunks; chunks that
403/// were already valid contribute nothing.
404#[derive(Debug, Clone, PartialEq, Eq)]
405pub struct CorrectionDetail {
406 /// 0-indexed position of the chunk in the caller's `&[&str]` slice.
407 pub chunk_index: usize,
408 /// 0-indexed position of the corrected character within the chunk's
409 /// data-part (post-HRP-and-separator).
410 pub position: usize,
411 /// The original (corrupted) character at this position.
412 pub was: char,
413 /// The corrected character at this position.
414 pub now: char,
415}
416
417/// Local codex32 alphabet (BIP 173 lowercase). Each char = one 5-bit
418/// symbol. Duplicated from `codex32.rs` (which keeps it private) so this
419/// module doesn't widen the codex32 public surface; the mapping is
420/// constant per BIP 173.
421const CODEX32_ALPHABET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
422
423/// BIP 173 separator character between HRP and data-part for md1 strings.
424const HRP_PREFIX: &str = "md1";
425
426/// Parse a single md1 chunk into its 5-bit data-part symbol vector.
427/// Returns the data-with-checksum symbols (i.e. all symbols after `md1`).
428/// Visual separators (whitespace + `-`) are stripped per codex32 convention.
429fn parse_chunk_symbols(chunk: &str, chunk_index: usize) -> Result<Vec<u8>, Error> {
430 let lower = chunk.to_ascii_lowercase();
431 if !lower.starts_with(HRP_PREFIX) {
432 return Err(Error::Codex32DecodeError(format!(
433 "chunk {chunk_index}: string does not start with HRP md1"
434 )));
435 }
436 let rest = &lower[HRP_PREFIX.len()..];
437 let mut symbols: Vec<u8> = Vec::with_capacity(rest.len());
438 for c in rest.chars() {
439 if c.is_whitespace() || c == '-' {
440 continue;
441 }
442 let lc = c as u8;
443 let sym = CODEX32_ALPHABET
444 .iter()
445 .position(|&b| b == lc)
446 .ok_or_else(|| {
447 Error::Codex32DecodeError(format!(
448 "chunk {chunk_index}: character {c:?} not in codex32 alphabet"
449 ))
450 })? as u8;
451 symbols.push(sym);
452 }
453 Ok(symbols)
454}
455
456/// Re-encode a 5-bit data-part symbol vector as a complete md1 string.
457fn encode_chunk_string(data_with_checksum: &[u8]) -> String {
458 let mut out = String::with_capacity(HRP_PREFIX.len() + data_with_checksum.len());
459 out.push_str(HRP_PREFIX);
460 for &v in data_with_checksum {
461 out.push(CODEX32_ALPHABET[(v & 0x1F) as usize] as char);
462 }
463 out
464}
465
466/// BCH-error-correcting decode for a chunk-set of md1 strings.
467///
468/// Per plan §1 Q1 lock — full-decode semantics: this is the single entry
469/// point that callers needing both "did anything get repaired?" AND "the
470/// fully-decoded descriptor" should use.
471///
472/// Algorithm:
473/// 1. For each chunk, parse the data-part into 5-bit symbols and compute
474/// the BCH polymod residue (`hrp_expand("md") || data_with_checksum`)
475/// XOR'd against [`crate::bch::MD_REGULAR_CONST`].
476/// 2. Residue `== 0` ⇒ chunk passes through unchanged.
477/// 3. Residue `!= 0` ⇒ invoke
478/// [`crate::bch_decode::decode_regular_errors`]. If `None`, return
479/// `Err(Error::TooManyErrors { chunk_index, bound: 8 })` per plan §2.B.4
480/// D29 error-mapping table.
481/// 4. Apply corrections to the chunk's symbol vector, re-encode as a
482/// fresh md1 string, and record one [`CorrectionDetail`] per repaired
483/// character.
484/// 5. After ALL chunks have been processed (any single uncorrectable
485/// chunk aborts atomically per plan §1 D28), forward the corrected
486/// chunk strings to [`reassemble`] to produce the [`Descriptor`].
487///
488/// On success returns `(Descriptor, Vec<CorrectionDetail>)`. The
489/// correction-detail vector is in (`chunk_index` ascending,
490/// `position` ascending within chunk) order; an empty vector means every
491/// input chunk was already a valid codeword.
492pub fn decode_with_correction(
493 strings: &[&str],
494) -> Result<(Descriptor, Vec<CorrectionDetail>), Error> {
495 if strings.is_empty() {
496 return Err(Error::ChunkSetEmpty);
497 }
498
499 let mut corrected_strings: Vec<String> = Vec::with_capacity(strings.len());
500 let mut all_details: Vec<CorrectionDetail> = Vec::new();
501
502 for (chunk_index, chunk) in strings.iter().enumerate() {
503 let symbols = parse_chunk_symbols(chunk, chunk_index)?;
504
505 // Polymod residue against md1's target constant.
506 let mut input = crate::bch::hrp_expand("md");
507 input.extend_from_slice(&symbols);
508 let residue = crate::bch::polymod_run(&input) ^ crate::bch::MD_REGULAR_CONST;
509
510 if residue == 0 {
511 // Already valid — pass through unchanged.
512 corrected_strings.push((*chunk).to_string());
513 continue;
514 }
515
516 // Attempt BCH correction.
517 let (positions, magnitudes) =
518 crate::bch_decode::decode_regular_errors(residue, symbols.len()).ok_or(
519 Error::TooManyErrors {
520 chunk_index,
521 bound: 8,
522 },
523 )?;
524
525 // Apply corrections; record (was, now) chars per position.
526 let mut corrected = symbols.clone();
527 let mut details: Vec<CorrectionDetail> = Vec::with_capacity(positions.len());
528 for (&pos, &mag) in positions.iter().zip(&magnitudes) {
529 if pos >= corrected.len() {
530 // Defensive: chien_search bounded pos to [0, L); but a
531 // pathological 5+-error pattern could in principle skirt
532 // that. Treat as uncorrectable per Q2 absorption rules.
533 return Err(Error::TooManyErrors {
534 chunk_index,
535 bound: 8,
536 });
537 }
538 let was_byte = corrected[pos];
539 let now_byte = was_byte ^ mag;
540 let was = CODEX32_ALPHABET[(was_byte & 0x1F) as usize] as char;
541 let now = CODEX32_ALPHABET[(now_byte & 0x1F) as usize] as char;
542 details.push(CorrectionDetail {
543 chunk_index,
544 position: pos,
545 was,
546 now,
547 });
548 corrected[pos] = now_byte;
549 }
550
551 // Defensive re-verify (catches pathological 5+-error patterns
552 // that happen to produce a degree-≤4 locator with 4 valid roots).
553 let mut verify_input = crate::bch::hrp_expand("md");
554 verify_input.extend_from_slice(&corrected);
555 let verify_residue =
556 crate::bch::polymod_run(&verify_input) ^ crate::bch::MD_REGULAR_CONST;
557 if verify_residue != 0 {
558 return Err(Error::TooManyErrors {
559 chunk_index,
560 bound: 8,
561 });
562 }
563
564 corrected_strings.push(encode_chunk_string(&corrected));
565 all_details.extend(details);
566 }
567
568 // Hand corrected strings to the existing reassembly path.
569 let corrected_refs: Vec<&str> = corrected_strings.iter().map(|s| s.as_str()).collect();
570 let descriptor = reassemble(&corrected_refs)?;
571 Ok((descriptor, all_details))
572}