1use crate::core::{box3d_lock, callback_state};
2use crate::debug_draw::{
3 CollectDebugDraw, DebugDraw, DebugDrawCommand, DebugDrawOptions, with_debug_draw,
4};
5use crate::error::{Error, Result};
6use crate::query::QueryFilter;
7use crate::types::{Aabb, BodyId, Pos, ShapeId, Vec3};
8use crate::world::World;
9use boxddd_sys::ffi;
10use std::ffi::{CStr, CString};
11use std::fs::File;
12use std::io::Write;
13use std::marker::PhantomData;
14use std::path::Path;
15use std::ptr::NonNull;
16use std::rc::Rc;
17use std::sync::{Mutex, OnceLock};
18
19#[derive(Copy, Clone, Debug)]
20struct ActiveRecording {
21 world: ffi::b3WorldId,
22 recording: usize,
23}
24
25static ACTIVE_RECORDINGS: OnceLock<Mutex<Vec<ActiveRecording>>> = OnceLock::new();
26
27fn active_recordings() -> &'static Mutex<Vec<ActiveRecording>> {
28 ACTIVE_RECORDINGS.get_or_init(|| Mutex::new(Vec::new()))
29}
30
31fn prune_inactive_recordings_locked(records: &mut Vec<ActiveRecording>) {
32 records.retain(|record| unsafe { ffi::b3World_IsValid(record.world) });
33}
34
35fn recording_key(recording: NonNull<ffi::b3Recording>) -> usize {
36 recording.as_ptr() as usize
37}
38
39fn active_recording_for_world_locked(world: ffi::b3WorldId) -> Option<usize> {
40 let mut records = active_recordings()
41 .lock()
42 .expect("Box3D recording registry lock poisoned");
43 prune_inactive_recordings_locked(&mut records);
44 records
45 .iter()
46 .find(|record| same_world_id(record.world, world))
47 .map(|record| record.recording)
48}
49
50fn insert_active_recording_locked(world: ffi::b3WorldId, recording: NonNull<ffi::b3Recording>) {
51 let mut records = active_recordings()
52 .lock()
53 .expect("Box3D recording registry lock poisoned");
54 prune_inactive_recordings_locked(&mut records);
55 records.push(ActiveRecording {
56 world,
57 recording: recording_key(recording),
58 });
59}
60
61fn remove_active_recording_locked(recording: NonNull<ffi::b3Recording>) {
62 let key = recording_key(recording);
63 let mut records = active_recordings()
64 .lock()
65 .expect("Box3D recording registry lock poisoned");
66 records.retain(|record| record.recording != key);
67}
68
69fn active_recording_matches_locked(
70 world: ffi::b3WorldId,
71 recording: NonNull<ffi::b3Recording>,
72) -> bool {
73 active_recording_for_world_locked(world) == Some(recording_key(recording))
74}
75
76pub(crate) fn detach_world_recording_locked(world: ffi::b3WorldId) {
77 let mut records = active_recordings()
78 .lock()
79 .expect("Box3D recording registry lock poisoned");
80 records.retain(|record| !same_world_id(record.world, world));
81}
82
83#[derive(Debug)]
85pub struct Recording {
86 raw: NonNull<ffi::b3Recording>,
87 active_world: Option<ffi::b3WorldId>,
88 _not_send_sync: PhantomData<Rc<()>>,
89}
90
91impl Recording {
92 pub fn new() -> Result<Self> {
94 Self::with_capacity(0)
95 }
96
97 pub fn with_capacity(byte_capacity: usize) -> Result<Self> {
99 let byte_capacity = i32::try_from(byte_capacity).map_err(|_| Error::InvalidArgument)?;
100 callback_state::check_not_in_callback()?;
101 let _guard = box3d_lock::lock();
102 let raw = unsafe { ffi::b3CreateRecording(byte_capacity) };
103 Ok(Self {
104 raw: NonNull::new(raw).ok_or(Error::CreateRecordingFailed)?,
105 active_world: None,
106 _not_send_sync: PhantomData,
107 })
108 }
109
110 pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
112 let path = path_to_cstring(path)?;
113 callback_state::check_not_in_callback()?;
114 let _guard = box3d_lock::lock();
115 let raw = unsafe { ffi::b3LoadRecordingFromFile(path.as_ptr()) };
116 Ok(Self {
117 raw: NonNull::new(raw).ok_or(Error::RecordingIoFailed)?,
118 active_world: None,
119 _not_send_sync: PhantomData,
120 })
121 }
122
123 pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
128 let path = path.as_ref();
129 callback_state::check_not_in_callback()?;
130 let bytes = {
131 let _guard = box3d_lock::lock();
132 if self.active_world_is_valid_locked() {
133 return Err(Error::ResourceLifetimeViolation);
134 }
135 unsafe { self.bytes_locked() }.to_vec()
136 };
137
138 let mut file = File::create(path).map_err(|_| Error::RecordingIoFailed)?;
139 file.write_all(&bytes)
140 .and_then(|_| file.flush())
141 .map_err(|_| Error::RecordingIoFailed)
142 }
143
144 pub fn len(&self) -> usize {
146 let _guard = box3d_lock::lock();
147 unsafe { ffi::b3Recording_GetSize(self.raw.as_ptr()) }.max(0) as usize
148 }
149
150 pub fn is_empty(&self) -> bool {
152 self.len() == 0
153 }
154
155 pub fn bytes(&self) -> Result<&[u8]> {
160 callback_state::check_not_in_callback()?;
161 let _guard = box3d_lock::lock();
162 if self.active_world_is_valid_locked() {
163 return Err(Error::ResourceLifetimeViolation);
164 }
165 Ok(unsafe { self.bytes_locked() })
166 }
167
168 pub fn to_vec(&self) -> Result<Vec<u8>> {
170 Ok(self.bytes()?.to_vec())
171 }
172
173 pub fn validate_replay(&self, worker_count: i32) -> Result<bool> {
175 validate_replay_bytes(self.bytes()?, worker_count)
176 }
177
178 pub fn create_player(&self, worker_count: i32) -> Result<RecPlayer> {
180 RecPlayer::from_bytes(self.bytes()?, worker_count)
181 }
182
183 #[inline]
184 fn active_world_is_valid_locked(&self) -> bool {
185 self.active_world
186 .is_some_and(|world| unsafe { ffi::b3World_IsValid(world) })
187 }
188
189 #[inline]
190 fn clear_inactive_world_locked(&mut self) {
191 if self
192 .active_world
193 .is_some_and(|world| !unsafe { ffi::b3World_IsValid(world) })
194 {
195 self.active_world = None;
196 }
197 }
198
199 #[inline]
200 unsafe fn bytes_locked(&self) -> &[u8] {
201 let size = unsafe { ffi::b3Recording_GetSize(self.raw.as_ptr()) }.max(0) as usize;
202 let data = unsafe { ffi::b3Recording_GetData(self.raw.as_ptr()) };
203 if data.is_null() || size == 0 {
204 &[]
205 } else {
206 unsafe { std::slice::from_raw_parts(data, size) }
207 }
208 }
209}
210
211impl Drop for Recording {
212 fn drop(&mut self) {
213 let _guard = box3d_lock::lock();
214 if let Some(world) = self.active_world.take()
215 && unsafe { ffi::b3World_IsValid(world) }
216 && active_recording_matches_locked(world, self.raw)
217 {
218 unsafe { ffi::b3World_StopRecording(world) };
219 }
220 remove_active_recording_locked(self.raw);
221 unsafe { ffi::b3DestroyRecording(self.raw.as_ptr()) };
222 }
223}
224
225#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
227#[derive(Copy, Clone, Debug, PartialEq, Eq)]
228pub enum RecQueryType {
229 OverlapAabb,
231 OverlapShape,
233 CastRay,
235 CastShape,
237 CastRayClosest,
239 CastMover,
241 CollideMover,
243}
244
245impl RecQueryType {
246 pub const fn from_raw(raw: ffi::b3RecQueryType) -> Option<Self> {
248 match raw {
249 ffi::b3RecQueryType_b3_recQueryOverlapAABB => Some(Self::OverlapAabb),
250 ffi::b3RecQueryType_b3_recQueryOverlapShape => Some(Self::OverlapShape),
251 ffi::b3RecQueryType_b3_recQueryCastRay => Some(Self::CastRay),
252 ffi::b3RecQueryType_b3_recQueryCastShape => Some(Self::CastShape),
253 ffi::b3RecQueryType_b3_recQueryCastRayClosest => Some(Self::CastRayClosest),
254 ffi::b3RecQueryType_b3_recQueryCastMover => Some(Self::CastMover),
255 ffi::b3RecQueryType_b3_recQueryCollideMover => Some(Self::CollideMover),
256 _ => None,
257 }
258 }
259}
260
261#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
263#[derive(Clone, Debug, PartialEq)]
264pub struct RecPlayerInfo {
265 pub frame_count: i32,
267 pub worker_count: i32,
269 pub time_step: f32,
271 pub sub_step_count: i32,
273 pub length_scale: f32,
275 pub bounds: Aabb,
277}
278
279impl RecPlayerInfo {
280 #[inline]
282 pub fn from_raw(raw: ffi::b3RecPlayerInfo) -> Self {
283 Self {
284 frame_count: raw.frameCount,
285 worker_count: raw.workerCount,
286 time_step: raw.timeStep,
287 sub_step_count: raw.subStepCount,
288 length_scale: raw.lengthScale,
289 bounds: Aabb::from_raw(raw.bounds),
290 }
291 }
292}
293
294#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
296#[derive(Clone, Debug, PartialEq)]
297pub struct RecQueryInfo {
298 pub query_type: RecQueryType,
300 pub filter: QueryFilter,
302 pub aabb: Aabb,
304 pub origin: Pos,
306 pub translation: Vec3,
308 pub hit_count: i32,
310 pub key: u64,
312 pub id: u64,
314 pub name: Option<String>,
316}
317
318impl RecQueryInfo {
319 fn from_raw(raw: ffi::b3RecQueryInfo) -> Result<Self> {
320 Ok(Self {
321 query_type: RecQueryType::from_raw(raw.type_).ok_or(Error::InvalidArgument)?,
322 filter: QueryFilter {
323 category_bits: raw.filter.categoryBits,
324 mask_bits: raw.filter.maskBits,
325 id: raw.filter.id,
326 },
327 aabb: Aabb::from_raw(raw.aabb),
328 origin: Pos::from_raw(raw.origin),
329 translation: Vec3::from_raw(raw.translation),
330 hit_count: raw.hitCount,
331 key: raw.key,
332 id: raw.id,
333 name: if raw.name.is_null() {
334 None
335 } else {
336 Some(
337 unsafe { CStr::from_ptr(raw.name) }
338 .to_string_lossy()
339 .into_owned(),
340 )
341 },
342 })
343 }
344}
345
346#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
348#[derive(Copy, Clone, Debug, PartialEq)]
349pub struct RecQueryHit {
350 pub shape_id: ShapeId,
352 pub point: Pos,
354 pub normal: Vec3,
356 pub fraction: f32,
358}
359
360impl RecQueryHit {
361 #[inline]
362 fn from_raw(raw: ffi::b3RecQueryHit) -> Self {
363 Self {
364 shape_id: ShapeId::from_raw(raw.shape),
365 point: Pos::from_raw(raw.point),
366 normal: Vec3::from_raw(raw.normal),
367 fraction: raw.fraction,
368 }
369 }
370}
371
372#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
374#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
375pub struct ReplayWorldId {
376 pub index1: u16,
378 pub generation: u16,
380}
381
382impl ReplayWorldId {
383 #[inline]
385 pub const fn from_raw(raw: ffi::b3WorldId) -> Self {
386 Self {
387 index1: raw.index1,
388 generation: raw.generation,
389 }
390 }
391
392 #[inline]
394 pub const fn into_raw(self) -> ffi::b3WorldId {
395 ffi::b3WorldId {
396 index1: self.index1,
397 generation: self.generation,
398 }
399 }
400
401 pub fn is_valid(self) -> bool {
403 let _guard = box3d_lock::lock();
404 unsafe { ffi::b3World_IsValid(self.into_raw()) }
405 }
406}
407
408#[derive(Debug)]
410pub struct RecPlayer {
411 raw: NonNull<ffi::b3RecPlayer>,
412 _not_send_sync: PhantomData<Rc<()>>,
413}
414
415impl RecPlayer {
416 pub fn from_bytes(bytes: &[u8], worker_count: i32) -> Result<Self> {
418 validate_replay_input(bytes, worker_count)?;
419 callback_state::check_not_in_callback()?;
420 let _guard = box3d_lock::lock();
421 let raw = unsafe {
422 ffi::b3RecPlayer_Create(bytes.as_ptr().cast(), bytes.len() as i32, worker_count)
423 };
424 Ok(Self {
425 raw: NonNull::new(raw).ok_or(Error::CreateRecPlayerFailed)?,
426 _not_send_sync: PhantomData,
427 })
428 }
429
430 pub fn world_id(&self) -> ReplayWorldId {
432 let _guard = box3d_lock::lock();
433 ReplayWorldId::from_raw(unsafe { ffi::b3RecPlayer_GetWorldId(self.raw.as_ptr()) })
434 }
435
436 pub fn step_frame(&mut self) -> Result<bool> {
438 callback_state::check_not_in_callback()?;
439 let _guard = box3d_lock::lock();
440 Ok(unsafe { ffi::b3RecPlayer_StepFrame(self.raw.as_ptr()) })
441 }
442
443 pub fn restart(&mut self) -> Result<()> {
445 callback_state::check_not_in_callback()?;
446 let _guard = box3d_lock::lock();
447 unsafe { ffi::b3RecPlayer_Restart(self.raw.as_ptr()) };
448 Ok(())
449 }
450
451 pub fn seek_frame(&mut self, target_frame: i32) -> Result<()> {
453 if target_frame < 0 {
454 return Err(Error::InvalidArgument);
455 }
456 callback_state::check_not_in_callback()?;
457 let _guard = box3d_lock::lock();
458 unsafe { ffi::b3RecPlayer_SeekFrame(self.raw.as_ptr(), target_frame) };
459 Ok(())
460 }
461
462 pub fn frame(&self) -> i32 {
464 let _guard = box3d_lock::lock();
465 unsafe { ffi::b3RecPlayer_GetFrame(self.raw.as_ptr()) }
466 }
467
468 pub fn frame_count(&self) -> i32 {
470 let _guard = box3d_lock::lock();
471 unsafe { ffi::b3RecPlayer_GetFrameCount(self.raw.as_ptr()) }
472 }
473
474 pub fn is_at_end(&self) -> bool {
476 let _guard = box3d_lock::lock();
477 unsafe { ffi::b3RecPlayer_IsAtEnd(self.raw.as_ptr()) }
478 }
479
480 pub fn has_diverged(&self) -> bool {
482 let _guard = box3d_lock::lock();
483 unsafe { ffi::b3RecPlayer_HasDiverged(self.raw.as_ptr()) }
484 }
485
486 pub fn info(&self) -> RecPlayerInfo {
488 let _guard = box3d_lock::lock();
489 RecPlayerInfo::from_raw(unsafe { ffi::b3RecPlayer_GetInfo(self.raw.as_ptr()) })
490 }
491
492 pub fn diverge_frame(&self) -> Option<i32> {
494 let _guard = box3d_lock::lock();
495 let frame = unsafe { ffi::b3RecPlayer_GetDivergeFrame(self.raw.as_ptr()) };
496 (frame >= 0).then_some(frame)
497 }
498
499 pub fn set_worker_count(&mut self, count: i32) -> Result<()> {
501 validate_replay_worker_count(count)?;
502 callback_state::check_not_in_callback()?;
503 let _guard = box3d_lock::lock();
504 unsafe { ffi::b3RecPlayer_SetWorkerCount(self.raw.as_ptr(), count) };
505 Ok(())
506 }
507
508 pub fn set_keyframe_policy(
510 &mut self,
511 budget_bytes: usize,
512 min_interval_frames: i32,
513 ) -> Result<()> {
514 if min_interval_frames < 0 {
515 return Err(Error::InvalidArgument);
516 }
517 callback_state::check_not_in_callback()?;
518 let _guard = box3d_lock::lock();
519 unsafe {
520 ffi::b3RecPlayer_SetKeyframePolicy(self.raw.as_ptr(), budget_bytes, min_interval_frames)
521 };
522 Ok(())
523 }
524
525 pub fn keyframe_budget(&self) -> usize {
527 let _guard = box3d_lock::lock();
528 unsafe { ffi::b3RecPlayer_GetKeyframeBudget(self.raw.as_ptr()) }
529 }
530
531 pub fn keyframe_min_interval(&self) -> i32 {
533 let _guard = box3d_lock::lock();
534 unsafe { ffi::b3RecPlayer_GetKeyframeMinInterval(self.raw.as_ptr()) }
535 }
536
537 pub fn keyframe_interval(&self) -> i32 {
539 let _guard = box3d_lock::lock();
540 unsafe { ffi::b3RecPlayer_GetKeyframeInterval(self.raw.as_ptr()) }
541 }
542
543 pub fn keyframe_bytes(&self) -> usize {
545 let _guard = box3d_lock::lock();
546 unsafe { ffi::b3RecPlayer_GetKeyframeBytes(self.raw.as_ptr()) }
547 }
548
549 pub fn body_count(&self) -> i32 {
551 let _guard = box3d_lock::lock();
552 unsafe { ffi::b3RecPlayer_GetBodyCount(self.raw.as_ptr()) }
553 }
554
555 pub fn body_id(&self, index: i32) -> Result<Option<BodyId>> {
557 if index < 0 {
558 return Err(Error::IndexOutOfRange);
559 }
560 let _guard = box3d_lock::lock();
561 let raw = unsafe { ffi::b3RecPlayer_GetBodyId(self.raw.as_ptr(), index) };
562 if raw.index1 == 0 {
563 Ok(None)
564 } else {
565 Ok(Some(BodyId::from_raw(raw)))
566 }
567 }
568
569 pub fn frame_query_count(&self) -> i32 {
571 let _guard = box3d_lock::lock();
572 unsafe { ffi::b3RecPlayer_GetFrameQueryCount(self.raw.as_ptr()) }
573 }
574
575 pub fn frame_query(&self, index: i32) -> Result<RecQueryInfo> {
577 if index < 0 || index >= self.frame_query_count() {
578 return Err(Error::IndexOutOfRange);
579 }
580 let _guard = box3d_lock::lock();
581 RecQueryInfo::from_raw(unsafe { ffi::b3RecPlayer_GetFrameQuery(self.raw.as_ptr(), index) })
582 }
583
584 pub fn frame_query_hit(&self, query_index: i32, hit_index: i32) -> Result<RecQueryHit> {
586 let query = self.frame_query(query_index)?;
587 if hit_index < 0 || hit_index >= query.hit_count {
588 return Err(Error::IndexOutOfRange);
589 }
590 let _guard = box3d_lock::lock();
591 Ok(RecQueryHit::from_raw(unsafe {
592 ffi::b3RecPlayer_GetFrameQueryHit(self.raw.as_ptr(), query_index, hit_index)
593 }))
594 }
595
596 pub fn draw_frame_queries_collect(
598 &mut self,
599 options: DebugDrawOptions,
600 query_index: Option<i32>,
601 selected_index: Option<i32>,
602 ) -> Result<Vec<DebugDrawCommand>> {
603 let mut commands = Vec::new();
604 self.draw_frame_queries_collect_into(&mut commands, options, query_index, selected_index)?;
605 Ok(commands)
606 }
607
608 pub fn draw_frame_queries_collect_into(
610 &mut self,
611 out: &mut Vec<DebugDrawCommand>,
612 options: DebugDrawOptions,
613 query_index: Option<i32>,
614 selected_index: Option<i32>,
615 ) -> Result<()> {
616 out.clear();
617 let mut collector = CollectDebugDraw::new(out);
618 self.draw_frame_queries(&mut collector, options, query_index, selected_index)?;
619 collector.finish();
620 Ok(())
621 }
622
623 pub fn draw_frame_queries(
625 &mut self,
626 drawer: &mut impl DebugDraw,
627 options: DebugDrawOptions,
628 query_index: Option<i32>,
629 selected_index: Option<i32>,
630 ) -> Result<()> {
631 callback_state::check_not_in_callback()?;
632 let query_index = checked_optional_index(query_index)?;
633 let selected_index = checked_optional_index(selected_index)?;
634 if query_index >= 0 && query_index >= self.frame_query_count() {
635 return Err(Error::IndexOutOfRange);
636 }
637 with_debug_draw(drawer, options, |draw| {
638 let _guard = box3d_lock::lock();
639 unsafe {
640 ffi::b3RecPlayer_DrawFrameQueries(
641 self.raw.as_ptr(),
642 draw,
643 query_index,
644 selected_index,
645 )
646 };
647 Ok(())
648 })
649 }
650}
651
652impl Drop for RecPlayer {
653 fn drop(&mut self) {
654 let _guard = box3d_lock::lock();
655 unsafe { ffi::b3RecPlayer_Destroy(self.raw.as_ptr()) };
656 }
657}
658
659impl World {
660 pub fn try_start_recording(&mut self, recording: &mut Recording) -> Result<()> {
662 callback_state::check_not_in_callback()?;
663 let _guard = box3d_lock::lock();
664 self.check_world_valid_locked()?;
665 recording.clear_inactive_world_locked();
666 if recording.active_world.is_some() {
667 return Err(Error::ResourceLifetimeViolation);
668 }
669 if active_recording_for_world_locked(self.raw()).is_some() {
670 return Err(Error::ResourceLifetimeViolation);
671 }
672 unsafe { ffi::b3World_StartRecording(self.raw(), recording.raw.as_ptr()) };
673 insert_active_recording_locked(self.raw(), recording.raw);
674 recording.active_world = Some(self.raw());
675 Ok(())
676 }
677
678 pub fn start_recording(&mut self, recording: &mut Recording) {
680 self.try_start_recording(recording)
681 .expect("Box3D failed to start recording");
682 }
683
684 pub fn try_stop_recording(&mut self, recording: &mut Recording) -> Result<()> {
686 callback_state::check_not_in_callback()?;
687 let _guard = box3d_lock::lock();
688 self.check_world_valid_locked()?;
689 recording.clear_inactive_world_locked();
690 let Some(active_world) = recording.active_world else {
691 return Err(Error::ResourceLifetimeViolation);
692 };
693 if !same_world_id(active_world, self.raw())
694 || !active_recording_matches_locked(self.raw(), recording.raw)
695 {
696 return Err(Error::ResourceLifetimeViolation);
697 }
698 unsafe { ffi::b3World_StopRecording(self.raw()) };
699 remove_active_recording_locked(recording.raw);
700 recording.active_world = None;
701 Ok(())
702 }
703
704 pub fn stop_recording(&mut self, recording: &mut Recording) {
706 self.try_stop_recording(recording)
707 .expect("Box3D failed to stop recording");
708 }
709}
710
711pub fn validate_replay_bytes(bytes: &[u8], worker_count: i32) -> Result<bool> {
713 validate_replay_input(bytes, worker_count)?;
714 callback_state::check_not_in_callback()?;
715 let _guard = box3d_lock::lock();
716 Ok(unsafe { ffi::b3ValidateReplay(bytes.as_ptr().cast(), bytes.len() as i32, worker_count) })
717}
718
719fn validate_replay_input(bytes: &[u8], worker_count: i32) -> Result<()> {
720 validate_replay_worker_count(worker_count)?;
721 if bytes.is_empty() || bytes.len() > i32::MAX as usize {
722 Err(Error::InvalidArgument)
723 } else {
724 Ok(())
725 }
726}
727
728fn validate_replay_worker_count(worker_count: i32) -> Result<()> {
729 if worker_count < 1 {
730 return Err(Error::InvalidArgument);
731 }
732 #[cfg(target_arch = "wasm32")]
733 if worker_count > 1 {
734 return Err(Error::UnsupportedOnWasm);
735 }
736 Ok(())
737}
738
739fn checked_optional_index(index: Option<i32>) -> Result<i32> {
740 match index {
741 Some(index) if index < 0 => Err(Error::IndexOutOfRange),
742 Some(index) => Ok(index),
743 None => Ok(-1),
744 }
745}
746
747fn same_world_id(a: ffi::b3WorldId, b: ffi::b3WorldId) -> bool {
748 a.index1 == b.index1 && a.generation == b.generation
749}
750
751fn path_to_cstring(path: impl AsRef<Path>) -> Result<CString> {
752 CString::new(path.as_ref().as_os_str().to_string_lossy().as_bytes())
753 .map_err(|_| Error::NulByteInString)
754}