// Oak Video Editor - Non-Linear Video Editor // Copyright (C) 2026 Oak Team // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Small helpers for crossing the Qt <-> C boundary, mirroring //! `include/common/qtutils.h`. No state and no handle. //! //! Port of `src/common/src/qtutils.{h,cpp}` and the thin wrapper in //! `src/common/c_api/qtutils.cpp`. use std::ffi::c_void; #[cfg(any(target_os = "macos", target_os = "freebsd"))] use std::ffi::{c_char, c_int, CString}; use crate::error::{Error, Result}; /// Convert an opaque pointer to its numeric representation. /// /// A null pointer yields `0`. Never fails; the `Result` mirrors the C ABI /// shape (the c_api only rejects a null `out_value`). pub fn ptr_to_value(ptr: *mut c_void) -> Result { // CPP-PARITY: `reinterpret_cast(ptr)`; zero-extended to 64 bits. Ok(ptr as usize as u64) } /// Convert a numeric representation back to an opaque pointer. /// /// `0` yields a null pointer. Never fails; the `Result` mirrors the C ABI /// shape (the c_api only rejects a null `out_ptr`). pub fn value_to_ptr(value: u64) -> Result<*mut c_void> { // CPP-PARITY: `reinterpret_cast(value)`, truncating to the pointer // width like a 32-bit `uintptr_t` cast would. Ok(value as usize as *mut c_void) } /// macOS/FreeBSD `struct timespec` mirror (`sys/types.h`). #[cfg(any(target_os = "macos", target_os = "freebsd"))] #[repr(C)] struct Timespec { tv_sec: i64, tv_nsec: i64, } /// macOS/FreeBSD `struct stat` mirror (`sys/stat.h`), laid out field-for-field /// so the raw `stat()` FFI below reads `st_birthtimespec` / `st_ctimespec`. #[cfg(any(target_os = "macos", target_os = "freebsd"))] #[repr(C)] struct Stat { st_dev: i32, st_mode: u16, st_nlink: u16, st_ino: u64, st_uid: u32, st_gid: u32, st_rdev: i32, _st_pad: u32, st_atimespec: Timespec, st_mtimespec: Timespec, st_ctimespec: Timespec, st_birthtimespec: Timespec, st_size: i64, st_blocks: i64, st_blksize: i32, st_flags: u32, st_gen: u32, st_lspare: i32, st_qspare: [i64; 2], } #[cfg(any(target_os = "macos", target_os = "freebsd"))] extern "C" { fn stat(path: *const c_char, buf: *mut Stat) -> c_int; } /// File creation time as seconds since the Unix epoch. /// /// On filesystems that record a birth time that value is used; otherwise the /// metadata change time is returned. Mirrors `olive::QtUtils::get_creation_date` /// plus the c_api wrapper, which turns a zero (epoch) result into /// `E_NOT_FOUND`. pub fn get_creation_date(path: &str) -> Result { #[cfg(any(target_os = "macos", target_os = "freebsd"))] { let cpath = CString::new(path).map_err(|_| Error::Invalid)?; let mut st = Stat { st_dev: 0, st_mode: 0, st_nlink: 0, st_ino: 0, st_uid: 0, st_gid: 0, st_rdev: 0, _st_pad: 0, st_atimespec: Timespec { tv_sec: 0, tv_nsec: 0, }, st_mtimespec: Timespec { tv_sec: 0, tv_nsec: 0, }, st_ctimespec: Timespec { tv_sec: 0, tv_nsec: 0, }, st_birthtimespec: Timespec { tv_sec: 0, tv_nsec: 0, }, st_size: 0, st_blocks: 0, st_blksize: 0, st_flags: 0, st_gen: 0, st_lspare: 0, st_qspare: [0, 0], }; if unsafe { stat(cpath.as_ptr(), &mut st) } != 0 { return Err(Error::NotFound); } // CPP-PARITY: birth time, falling back to the metadata change time when // the birth time is unset (0 / -1) — qtutils.cpp on macOS/FreeBSD. let mut secs = st.st_birthtimespec.tv_sec; if secs == 0 || secs == -1 { secs = st.st_ctimespec.tv_sec; } // CPP-PARITY: the c_api maps a zero (epoch) result to E_NOT_FOUND. if secs == 0 { return Err(Error::NotFound); } Ok(secs) } #[cfg(not(any(target_os = "macos", target_os = "freebsd")))] { let meta = std::fs::metadata(path).map_err(|_| Error::NotFound)?; // CPP-PARITY: `st_ctime` (metadata change time) has no std::fs // equivalent; fall back to creation then modification time. let t = meta .created() .or_else(|_| meta.modified()) .map_err(|_| Error::NotFound)?; let secs = t .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0); if secs == 0 { return Err(Error::NotFound); } Ok(secs) } } #[cfg(test)] mod tests { use std::ffi::c_void; use std::sync::atomic::{AtomicU64, Ordering}; use super::*; use crate::error::{Error, Result}; #[test] fn ptr_to_value_null_is_zero() { assert_eq!(ptr_to_value(std::ptr::null_mut()).unwrap(), 0); } #[test] fn ptr_value_round_trip() { let value: u64 = 0x1234_5678_9abc_def0; let ptr = value_to_ptr(value).unwrap(); assert_eq!(ptr_to_value(ptr).unwrap(), value); assert!(!ptr.is_null()); } #[test] fn value_to_ptr_null_is_null() { assert!(value_to_ptr(0).unwrap().is_null()); } #[test] fn value_to_ptr_accepts_any_u64() { // Mirrors `reinterpret_cast` which never rejects an input. assert!(value_to_ptr(u64::MAX).is_ok()); assert!(value_to_ptr(1).is_ok()); } #[test] fn ptr_to_value_of_real_pointer_round_trips() { let data = 42i32; let raw = &data as *const i32 as *mut c_void; let n = ptr_to_value(raw).unwrap(); let back = value_to_ptr(n).unwrap(); assert_eq!(back as *const i32 as *const i32, &data as *const i32); } // Stable temp-file helper: unique path under the system temp dir. fn temp_file_path(tag: &str) -> std::path::PathBuf { static COUNTER: AtomicU64 = AtomicU64::new(0); let n = COUNTER.fetch_add(1, Ordering::Relaxed); std::env::temp_dir().join(format!( "oakcommon_qtutils_{}_{}_{}.tmp", std::process::id(), tag, n )) } #[test] fn creation_date_of_existing_file() { let p = temp_file_path("exists"); std::fs::write(&p, b"hello").unwrap(); let secs = get_creation_date(p.to_str().unwrap()); // On macOS both paths read the same birth time; just assert success // and plausibility rather than exact equality across fs providers. match secs { Ok(s) => assert!(s > 0, "creation time should be in the past"), Err(e) => panic!("expected Ok, got {e:?}"), } // Cross-check against the std metadata creation time when available. if let Ok(meta) = std::fs::metadata(&p) { if let Ok(created) = meta.created() { let expected = created .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0); assert_eq!(secs.unwrap(), expected); } } let _ = std::fs::remove_file(&p); } #[test] fn creation_date_matches_std_for_new_file() { let p = temp_file_path("match"); std::fs::write(&p, b"data").unwrap(); let ours = get_creation_date(p.to_str().unwrap()).unwrap(); let expected = std::fs::metadata(&p) .and_then(|m| m.created()) .map(|t| t.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64) .unwrap_or(0); assert!(expected > 0); assert_eq!(ours, expected); let _ = std::fs::remove_file(&p); } #[test] fn creation_date_missing_file_is_not_found() { let p = temp_file_path("missing"); let _ = std::fs::remove_file(&p); let res = get_creation_date(p.to_str().unwrap()); assert!(matches!(res, Err(Error::NotFound))); } #[test] fn creation_date_empty_path_is_not_found() { assert!(matches!(get_creation_date(""), Err(Error::NotFound))); } #[test] fn creation_date_returns_result_ok_type() { let p = temp_file_path("typetest"); std::fs::write(&p, b"x").unwrap(); let res: Result = get_creation_date(p.to_str().unwrap()); assert!(res.is_ok()); let _ = std::fs::remove_file(&p); } #[test] fn value_to_ptr_is_injective_over_u64() { // Distinct values that survive the pointer-width cast map back distinctly. let a = value_to_ptr(1).unwrap(); let b = value_to_ptr(2).unwrap(); assert_ne!(a, b); } }