1use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6
7use crate::diagnostics::Warning;
8use crate::ewah_bitmap::{Bitmap, EwahView};
9use crate::midx::resolve_midx_bitmap_layer;
10use crate::objects::{ObjectId, ObjectKind};
11use crate::pack::PackIndex;
12use crate::pack_map::PackData;
13use crate::pack_rev::hashfile_checksum_valid;
14use crate::repo::Repository;
15
16use super::error::BitmapError;
17use super::order::BitmapOrder;
18use crate::pack::read_local_pack_indexes;
19
20const BITMAP_MAGIC: &[u8; 4] = b"BITM";
21const BITMAP_VERSION: u16 = 1;
22const BITMAP_OPT_FULL_DAG: u16 = 0x1;
23const BITMAP_OPT_HASH_CACHE: u16 = 0x4;
24const BITMAP_OPT_LOOKUP_TABLE: u16 = 0x10;
25const BITMAP_OPT_PSEUDO_MERGES: u16 = 0x20;
26const LOOKUP_TRIPLET_WIDTH: usize = 16;
27const MAX_XOR_OFFSET: usize = 160;
28const XOR_ROW_NONE: u32 = 0xffff_ffff;
29
30#[derive(Debug, Clone)]
32pub struct CommitReachabilityBitmap {
33 bits: Arc<Bitmap>,
34}
35
36impl CommitReachabilityBitmap {
37 #[must_use]
39 pub fn contains(&self, position: u32) -> bool {
40 self.bits.get(position as usize)
41 }
42
43 pub(crate) fn or_into(&self, dest: &mut Bitmap) {
45 dest.or_assign(&self.bits);
46 }
47
48 pub fn positions(&self) -> impl Iterator<Item = u32> + '_ {
50 self.bits
51 .set_bits()
52 .map(|p| u32::try_from(p).unwrap_or(u32::MAX))
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
58pub struct TypeBitmap<'a> {
59 view: EwahView<'a>,
60}
61
62impl TypeBitmap<'_> {
63 pub fn contains(&self, position: u32) -> Result<bool, BitmapError> {
69 let mut scratch = Bitmap::new();
70 self.view
71 .expand_into(&mut scratch)
72 .map_err(|_| BitmapError::InvalidEwah)?;
73 Ok(scratch.get(position as usize))
74 }
75
76 pub(crate) fn expand_into_bitmap(&self, dest: &mut Bitmap) -> Result<(), BitmapError> {
82 self.view
83 .expand_into(dest)
84 .map_err(|_| BitmapError::InvalidEwah)
85 }
86}
87
88pub struct BitmapIndex {
90 map: Arc<PackData>,
91 entry_count: u32,
92 has_lookup_table: bool,
93 order: BitmapOrder,
94 type_commits_off: usize,
95 type_trees_off: usize,
96 type_blobs_off: usize,
97 type_tags_off: usize,
98 name_hashes: Option<(usize, usize)>,
99 lookup_table: Option<(usize, usize)>,
100 eager_entries: Option<Vec<EagerEntry>>,
101 decode_cache: Mutex<HashMap<ObjectId, Arc<Bitmap>>>,
102}
103
104#[derive(Debug, Clone)]
105struct EagerEntry {
106 oid: ObjectId,
107 ewah_offset: usize,
108 xor_prev_index: Option<usize>,
109}
110
111#[derive(Debug, Clone, Copy)]
112struct LookupTriplet {
113 commit_pos: u32,
114 offset: u64,
115 xor_row: u32,
116}
117
118impl BitmapIndex {
119 pub fn open(repo: &Repository) -> Result<Option<Arc<Self>>, BitmapError> {
128 if !repo.odb().uses_files_primary() {
129 return Ok(None);
130 }
131 if let Some(cached) = repo.caches().bitmap_index().get() {
132 return Ok(Some(cached));
133 }
134 let objects_dir = repo.odb().files_objects_dir().ok_or(BitmapError::Corrupt)?;
135 let loaded = try_open_bitmap(objects_dir, repo)?;
136 repo.caches().bitmap_index().set(loaded.clone());
137 Ok(loaded)
138 }
139
140 pub(crate) fn backing_pack_index(&self) -> Option<Arc<PackIndex>> {
142 match &self.order {
143 super::order::BitmapOrder::Pack { index, .. } => Some(Arc::clone(index)),
144 super::order::BitmapOrder::Midx { .. } => None,
145 }
146 }
147
148 #[must_use]
150 pub fn commit_bitmap(&self, commit: &ObjectId) -> Option<CommitReachabilityBitmap> {
151 let bits = self.decode_commit_bitmap(commit).ok()??;
152 Some(CommitReachabilityBitmap { bits })
153 }
154
155 #[must_use]
157 pub fn type_bitmap(&self, kind: ObjectKind) -> TypeBitmap<'_> {
158 let off = match kind {
159 ObjectKind::Commit => self.type_commits_off,
160 ObjectKind::Tree => self.type_trees_off,
161 ObjectKind::Blob => self.type_blobs_off,
162 ObjectKind::Tag => self.type_tags_off,
163 };
164 TypeBitmap {
165 view: self.type_ewah_view(off),
166 }
167 }
168
169 #[must_use]
171 pub fn name_hash(&self, position: u32) -> Option<u32> {
172 let (start, end) = self.name_hashes?;
173 let row = self.order.storage_row_for_bitmap_rank(position)?;
174 let base = start + usize::try_from(row).ok()?.checked_mul(4)?;
175 if base + 4 > end {
176 return None;
177 }
178 let slice = self.map.get(base..base + 4)?;
179 Some(u32::from_be_bytes(slice.try_into().ok()?))
180 }
181
182 #[must_use]
184 pub fn object_count(&self) -> u32 {
185 self.order.object_count()
186 }
187
188 #[must_use]
190 pub fn has_lookup_table(&self) -> bool {
191 self.has_lookup_table
192 }
193
194 #[must_use]
196 pub fn selected_commit_count(&self) -> u32 {
197 self.entry_count
198 }
199
200 #[must_use]
202 pub(crate) fn pack_index_row_for_bitmap_position(&self, position: u32) -> Option<u32> {
203 self.order.storage_row_for_bitmap_rank(position)
204 }
205
206 pub fn oid_at(&self, position: u32) -> Result<ObjectId, BitmapError> {
212 self.order
213 .oid_at(position)
214 .map_err(|_| BitmapError::Corrupt)
215 }
216
217 #[must_use]
219 pub fn position_of(&self, oid: &ObjectId) -> Option<u32> {
220 self.order.position_of(oid)
221 }
222
223 fn ewah_at(&self, offset: usize) -> Result<EwahView<'_>, BitmapError> {
224 let (view, _) = EwahView::parse(self.map.get(offset..).ok_or(BitmapError::Corrupt)?)
225 .map_err(|_| BitmapError::InvalidEwah)?;
226 Ok(view)
227 }
228
229 #[allow(clippy::expect_used)]
231 fn type_ewah_view(&self, offset: usize) -> EwahView<'_> {
232 self.ewah_at(offset)
233 .expect("type bitmap EWAH validated at open")
234 }
235
236 fn decode_commit_bitmap(&self, commit: &ObjectId) -> Result<Option<Arc<Bitmap>>, BitmapError> {
237 if let Ok(guard) = self.decode_cache.lock() {
238 if let Some(bits) = guard.get(commit) {
239 return Ok(Some(Arc::clone(bits)));
240 }
241 }
242 let Some(bits) = self.decode_commit_bitmap_uncached(commit)? else {
243 return Ok(None);
244 };
245 let arc = Arc::new(bits);
246 if let Ok(mut guard) = self.decode_cache.lock() {
247 guard.insert(*commit, Arc::clone(&arc));
248 }
249 Ok(Some(arc))
250 }
251
252 fn decode_commit_bitmap_uncached(
253 &self,
254 commit: &ObjectId,
255 ) -> Result<Option<Bitmap>, BitmapError> {
256 if self.has_lookup_table {
257 return self.decode_via_lookup_table(commit);
258 }
259 let Some(entries) = self.eager_entries.as_ref() else {
260 return Ok(None);
261 };
262 if let Some(idx) = entries.iter().position(|e| e.oid == *commit) {
263 return self.decode_eager_index(idx);
264 }
265 Ok(None)
266 }
267
268 fn decode_eager_index(&self, idx: usize) -> Result<Option<Bitmap>, BitmapError> {
269 let entries = self.eager_entries.as_ref().ok_or(BitmapError::Corrupt)?;
270 let entry = entries.get(idx).ok_or(BitmapError::Corrupt)?;
271 let xor = if let Some(prev) = entry.xor_prev_index {
272 Some(self.decode_eager_index(prev)?.ok_or(BitmapError::Corrupt)?)
273 } else {
274 None
275 };
276 self.decode_ewah_payload_at(entry.ewah_offset, xor)
277 .map(Some)
278 }
279
280 fn decode_via_lookup_table(&self, commit: &ObjectId) -> Result<Option<Bitmap>, BitmapError> {
281 let commit_pos = match self.order.storage_row_for_oid(commit) {
282 Some(p) => p,
283 None => return Ok(None),
284 };
285 let (start, end) = self.lookup_table.ok_or(BitmapError::Corrupt)?;
286 let table = self.map.get(start..end).ok_or(BitmapError::Corrupt)?;
287 let Some(triplet) = lookup_triplet_by_commit_pos(table, self.entry_count, commit_pos)
288 else {
289 return Ok(None);
290 };
291 let mut xor_base: Option<Bitmap> = None;
292 if triplet.xor_row != XOR_ROW_NONE {
293 xor_base = Some(self.decode_xor_chain(triplet.xor_row)?);
294 }
295 self.decode_entry_at(triplet.offset as usize, xor_base)
296 .map(Some)
297 }
298
299 fn decode_xor_chain(&self, mut xor_row: u32) -> Result<Bitmap, BitmapError> {
300 let (start, end) = self.lookup_table.ok_or(BitmapError::Corrupt)?;
301 let table = self.map.get(start..end).ok_or(BitmapError::Corrupt)?;
302 let mut stack: Vec<LookupTriplet> = Vec::new();
303 let mut steps = 0u32;
304 while xor_row != XOR_ROW_NONE {
305 steps += 1;
306 if steps > self.entry_count {
307 return Err(BitmapError::Corrupt);
308 }
309 let triplet = lookup_triplet_by_row(table, xor_row).ok_or(BitmapError::Corrupt)?;
310 if let Ok(oid) = self.order.oid_at_storage_row(triplet.commit_pos) {
311 if let Ok(guard) = self.decode_cache.lock() {
312 if let Some(existing) = guard.get(&oid) {
313 return Ok((**existing).clone());
314 }
315 }
316 }
317 stack.push(triplet);
318 xor_row = triplet.xor_row;
319 }
320 let mut acc: Option<Bitmap> = None;
321 while let Some(t) = stack.pop() {
322 acc = Some(self.decode_entry_at(t.offset as usize, acc)?);
323 if let Ok(oid) = self.order.oid_at_storage_row(t.commit_pos) {
324 if let Some(ref composed) = acc {
325 if let Ok(mut guard) = self.decode_cache.lock() {
326 guard.insert(oid, Arc::new(composed.clone()));
327 }
328 }
329 }
330 }
331 acc.ok_or(BitmapError::Corrupt)
332 }
333
334 fn decode_entry_at(
336 &self,
337 entry_start: usize,
338 xor_with: Option<Bitmap>,
339 ) -> Result<Bitmap, BitmapError> {
340 let data = self.map.as_ref();
341 if entry_start + 6 > data.len() {
342 return Err(BitmapError::Corrupt);
343 }
344 let ewah_start = entry_start + 6;
345 self.decode_ewah_payload_at(ewah_start, xor_with)
346 }
347
348 fn decode_ewah_payload_at(
350 &self,
351 ewah_start: usize,
352 xor_with: Option<Bitmap>,
353 ) -> Result<Bitmap, BitmapError> {
354 let (view, _) = EwahView::parse(self.map.get(ewah_start..).ok_or(BitmapError::Corrupt)?)
355 .map_err(|_| BitmapError::InvalidEwah)?;
356 let mut out = xor_with.unwrap_or_default();
357 let mut xor_layer = Bitmap::new();
358 view.expand_into(&mut xor_layer)
359 .map_err(|_| BitmapError::InvalidEwah)?;
360 out.xor_assign(&xor_layer);
361 Ok(out)
362 }
363}
364
365fn try_open_bitmap(
366 objects_dir: &Path,
367 repo: &Repository,
368) -> Result<Option<Arc<BitmapIndex>>, BitmapError> {
369 let pack_dir = objects_dir.join("pack");
370 if let Some((bitmap_path, midx_path)) = resolve_midx_bitmap_layer(&pack_dir) {
371 match open_bitmap_file(
372 &bitmap_path,
373 objects_dir,
374 BitmapSource::Midx { midx_path },
375 repo,
376 ) {
377 Ok(idx) => return Ok(Some(Arc::new(idx))),
378 Err(err) => warn_bitmap(repo, &bitmap_path, &err),
379 }
380 }
381 let mut pack_indexes =
382 read_local_pack_indexes(objects_dir).map_err(|e| BitmapError::Io(e.to_string()))?;
383 pack_indexes.sort_by_key(|idx| std::cmp::Reverse(idx.len()));
384 for idx in pack_indexes {
385 let stem = idx
386 .idx_path
387 .file_stem()
388 .and_then(|s| s.to_str())
389 .unwrap_or("pack");
390 let bitmap_path = pack_dir.join(format!("{stem}.bitmap"));
391 if !bitmap_path.is_file() {
392 continue;
393 }
394 let index_arc = Arc::new(idx);
395 match open_bitmap_file(
396 &bitmap_path,
397 objects_dir,
398 BitmapSource::Pack {
399 index: Arc::clone(&index_arc),
400 },
401 repo,
402 ) {
403 Ok(index) => return Ok(Some(Arc::new(index))),
404 Err(err) => warn_bitmap(repo, &bitmap_path, &err),
405 }
406 }
407 Ok(None)
408}
409
410enum BitmapSource {
411 Pack { index: Arc<PackIndex> },
412 Midx { midx_path: PathBuf },
413}
414
415fn warn_bitmap(repo: &Repository, path: &Path, err: &BitmapError) {
416 repo.caches()
417 .diagnostics_handle()
418 .warn(Warning::PackBitmapIgnored {
419 path: path.to_path_buf(),
420 reason: err.to_string(),
421 });
422}
423
424fn open_bitmap_file(
425 path: &Path,
426 objects_dir: &Path,
427 source: BitmapSource,
428 repo: &Repository,
429) -> Result<BitmapIndex, BitmapError> {
430 let map = PackData::open(path).map_err(|e| BitmapError::Io(e.to_string()))?;
431 let data = map.as_ref();
432 if data.is_empty() {
433 return Err(BitmapError::TooSmall);
434 }
435 let hash_len = repo.odb().hash_algo().len();
436 if !hashfile_checksum_valid(data, hash_len) {
437 return Err(BitmapError::FileChecksumMismatch);
438 }
439 let header_size = 12 + hash_len;
440 if data.len() < header_size + hash_len {
441 return Err(BitmapError::TooSmall);
442 }
443 if &data[0..4] != BITMAP_MAGIC {
444 return Err(BitmapError::InvalidHeader);
445 }
446 let version = u16::from_be_bytes([data[4], data[5]]);
447 if version != BITMAP_VERSION {
448 return Err(BitmapError::InvalidHeader);
449 }
450 let flags = u16::from_be_bytes([data[6], data[7]]);
451 if flags & BITMAP_OPT_FULL_DAG == 0 {
452 return Err(BitmapError::MissingFullDag);
453 }
454 let entry_count = u32::from_be_bytes(data[8..12].try_into().map_err(|_| BitmapError::Corrupt)?);
455 let header_checksum = &data[12..12 + hash_len];
456 verify_source_checksum(&source, objects_dir, header_checksum, hash_len)?;
457
458 let mut pos = header_size;
459 let type_commits_off = pos;
460 pos += ewah_consumed(data, pos)?;
461 let type_trees_off = pos;
462 pos += ewah_consumed(data, pos)?;
463 let type_blobs_off = pos;
464 pos += ewah_consumed(data, pos)?;
465 let type_tags_off = pos;
466 pos += ewah_consumed(data, pos)?;
467
468 let order = match &source {
469 BitmapSource::Pack { index } => BitmapOrder::load_pack(objects_dir, Arc::clone(index))
470 .map_err(|_| BitmapError::Corrupt)?,
471 BitmapSource::Midx { midx_path } => BitmapOrder::load_midx_file(midx_path)
472 .map_err(|_| BitmapError::Corrupt)?
473 .ok_or(BitmapError::Corrupt)?,
474 };
475
476 let num_objects = order.object_count() as usize;
477 let hash_cache_len = num_objects.checked_mul(4).unwrap_or(0);
478 let mut tail_end = data.len() - hash_len;
479 let mut name_hashes = None;
480 let mut lookup_table = None;
481
482 if flags & BITMAP_OPT_HASH_CACHE != 0 {
483 if hash_cache_len > tail_end.saturating_sub(header_size) {
484 return Err(BitmapError::Corrupt);
485 }
486 tail_end -= hash_cache_len;
487 name_hashes = Some((tail_end, tail_end + hash_cache_len));
488 }
489 if flags & BITMAP_OPT_LOOKUP_TABLE != 0 {
490 let table_len = entry_count as usize * LOOKUP_TRIPLET_WIDTH;
491 if table_len > tail_end.saturating_sub(header_size) {
492 return Err(BitmapError::Corrupt);
493 }
494 tail_end -= table_len;
495 lookup_table = Some((tail_end, tail_end + table_len));
496 }
497 if flags & BITMAP_OPT_PSEUDO_MERGES != 0 {
498 tail_end = skip_pseudo_merge_extension(data, header_size, tail_end)?;
499 }
500
501 let entries_start = pos;
502 let eager_entries = if lookup_table.is_some() {
503 None
504 } else {
505 Some(load_eager_entries(
506 data,
507 entries_start,
508 entry_count,
509 &order,
510 tail_end,
511 )?)
512 };
513
514 Ok(BitmapIndex {
515 map,
516 entry_count,
517 has_lookup_table: lookup_table.is_some(),
518 order,
519 type_commits_off,
520 type_trees_off,
521 type_blobs_off,
522 type_tags_off,
523 name_hashes,
524 lookup_table,
525 eager_entries,
526 decode_cache: Mutex::new(HashMap::new()),
527 })
528}
529
530fn verify_source_checksum(
531 source: &BitmapSource,
532 _objects_dir: &Path,
533 header_checksum: &[u8],
534 hash_len: usize,
535) -> Result<(), BitmapError> {
536 let path = match source {
537 BitmapSource::Pack { index } => &index.pack_path,
538 BitmapSource::Midx { midx_path } => midx_path,
539 };
540 match read_file_trailer(path, hash_len).map_err(BitmapError::io)? {
541 Some(trailer) if trailer == header_checksum => Ok(()),
542 _ => Err(BitmapError::PackChecksumMismatch),
543 }
544}
545
546fn read_file_trailer(path: &Path, len: usize) -> std::io::Result<Option<Vec<u8>>> {
550 use std::io::{Read, Seek, SeekFrom};
551
552 let mut file = std::fs::File::open(path)?;
553 let file_len = file.metadata()?.len();
554 let Ok(len_u64) = u64::try_from(len) else {
555 return Ok(None);
556 };
557 if file_len < len_u64 {
558 return Ok(None);
559 }
560 file.seek(SeekFrom::Start(file_len - len_u64))?;
561 let mut trailer = vec![0u8; len];
562 file.read_exact(&mut trailer)?;
563 Ok(Some(trailer))
564}
565
566fn skip_pseudo_merge_extension(
567 data: &[u8],
568 header_size: usize,
569 mut tail_end: usize,
570) -> Result<usize, BitmapError> {
571 if tail_end < header_size + 8 {
572 return Err(BitmapError::Corrupt);
573 }
574 let ext_size = u64::from_be_bytes(
575 data[tail_end - 8..tail_end]
576 .try_into()
577 .map_err(|_| BitmapError::Corrupt)?,
578 );
579 let ext_size = usize::try_from(ext_size).map_err(|_| BitmapError::Corrupt)?;
580 if ext_size > tail_end.saturating_sub(header_size) {
581 return Err(BitmapError::Corrupt);
582 }
583 tail_end -= ext_size;
584 Ok(tail_end)
585}
586
587fn ewah_consumed(data: &[u8], pos: usize) -> Result<usize, BitmapError> {
588 let (_, consumed) = EwahView::parse(data.get(pos..).ok_or(BitmapError::Corrupt)?)
589 .map_err(|_| BitmapError::InvalidEwah)?;
590 Ok(consumed)
591}
592
593fn load_eager_entries(
594 data: &[u8],
595 mut pos: usize,
596 entry_count: u32,
597 order: &BitmapOrder,
598 body_end: usize,
599) -> Result<Vec<EagerEntry>, BitmapError> {
600 let mut entries = Vec::with_capacity(entry_count as usize);
601 for i in 0..entry_count {
602 if pos + 6 > body_end {
603 return Err(BitmapError::Corrupt);
604 }
605 let commit_pos = u32::from_be_bytes(
606 data[pos..pos + 4]
607 .try_into()
608 .map_err(|_| BitmapError::Corrupt)?,
609 );
610 pos += 4;
611 let xor_offset = data[pos];
612 pos += 1;
613 let _flags = data[pos];
614 pos += 1;
615 let ewah_offset = pos;
616 pos += ewah_consumed(data, pos)?;
617 let oid = order
618 .oid_at_storage_row(commit_pos)
619 .map_err(|_| BitmapError::Corrupt)?;
620 let xor_prev_index = if xor_offset == 0 {
621 None
622 } else {
623 let x = xor_offset as usize;
624 if x > i as usize || x > MAX_XOR_OFFSET {
625 return Err(BitmapError::Corrupt);
626 }
627 Some(i as usize - x)
628 };
629 entries.push(EagerEntry {
630 oid,
631 ewah_offset,
632 xor_prev_index,
633 });
634 }
635 Ok(entries)
636}
637
638fn lookup_triplet_by_commit_pos(
639 table: &[u8],
640 entry_count: u32,
641 commit_pos: u32,
642) -> Option<LookupTriplet> {
643 let mut lo = 0usize;
644 let mut hi = entry_count as usize;
645 while lo < hi {
646 let mid = lo + (hi - lo) / 2;
647 let triplet = lookup_triplet_by_row(table, mid as u32)?;
648 match triplet.commit_pos.cmp(&commit_pos) {
649 std::cmp::Ordering::Less => lo = mid + 1,
650 std::cmp::Ordering::Greater => hi = mid,
651 std::cmp::Ordering::Equal => return Some(triplet),
652 }
653 }
654 None
655}
656
657fn lookup_triplet_by_row(table: &[u8], row: u32) -> Option<LookupTriplet> {
658 let base = row as usize * LOOKUP_TRIPLET_WIDTH;
659 let slice = table.get(base..base + LOOKUP_TRIPLET_WIDTH)?;
660 Some(LookupTriplet {
661 commit_pos: u32::from_be_bytes(slice[0..4].try_into().ok()?),
662 offset: u64::from_be_bytes(slice[4..12].try_into().ok()?),
663 xor_row: u32::from_be_bytes(slice[12..16].try_into().ok()?),
664 })
665}
666
667#[derive(Default)]
669pub struct BitmapIndexCache {
670 inner: std::sync::OnceLock<Option<Arc<BitmapIndex>>>,
671}
672
673impl BitmapIndexCache {
674 pub(crate) fn get(&self) -> Option<Arc<BitmapIndex>> {
675 self.inner.get().and_then(|opt| opt.clone())
676 }
677
678 pub(crate) fn set(&self, value: Option<Arc<BitmapIndex>>) {
679 let _ = self.inner.set(value);
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::read_file_trailer;
686
687 #[test]
688 fn read_file_trailer_returns_last_bytes_only() -> std::io::Result<()> {
689 let dir = tempfile::tempdir()?;
690 let path = dir.path().join("data");
691 let mut body = vec![0u8; 64 * 1024];
692 body.extend_from_slice(b"0123456789abcdefghij");
693 std::fs::write(&path, &body)?;
694 assert_eq!(
695 read_file_trailer(&path, 20)?.as_deref(),
696 Some(&b"0123456789abcdefghij"[..])
697 );
698 assert_eq!(read_file_trailer(&path, 0)?.as_deref(), Some(&b""[..]));
699
700 let short = dir.path().join("short");
701 std::fs::write(&short, b"abc")?;
702 assert_eq!(read_file_trailer(&short, 20)?, None);
703 assert_eq!(read_file_trailer(&short, 3)?.as_deref(), Some(&b"abc"[..]));
704 Ok(())
705 }
706}