1use std::{
7 ffi::CString,
8 path::{Path, PathBuf},
9};
10
11use crate::err::ReadStatError;
12
13const IN_EXTENSIONS: &[&str] = &["sas7bdat", "sas7bcat"];
14
15#[derive(Debug, Clone)]
20pub struct ReadStatPath {
21 pub path: PathBuf,
23 pub extension: String,
25 pub cstring_path: CString,
27}
28
29impl ReadStatPath {
30 #[allow(clippy::needless_pass_by_value)]
36 pub fn new(path: PathBuf) -> Result<Self, ReadStatError> {
37 let p = Self::validate_path(&path)?;
38 let ext = Self::validate_in_extension(&p)?;
39 let csp = Self::path_to_cstring(&p)?;
40
41 Ok(Self {
42 path: p,
43 extension: ext,
44 cstring_path: csp,
45 })
46 }
47
48 #[cfg(unix)]
50 pub(crate) fn path_to_cstring(path: &Path) -> Result<CString, ReadStatError> {
51 use std::os::unix::ffi::OsStrExt;
52 let bytes = path.as_os_str().as_bytes();
53 Ok(CString::new(bytes)?)
54 }
55
56 #[cfg(not(unix))]
58 pub(crate) fn path_to_cstring(path: &Path) -> Result<CString, ReadStatError> {
59 let rust_str = path
60 .as_os_str()
61 .to_str()
62 .ok_or_else(|| ReadStatError::Other("Invalid path".to_string()))?;
63 Ok(CString::new(rust_str)?)
64 }
65
66 fn validate_in_extension(path: &Path) -> Result<String, ReadStatError> {
67 path.extension()
68 .and_then(|e| e.to_str())
69 .map(std::borrow::ToOwned::to_owned)
70 .map_or(
71 Err(ReadStatError::Other(format!(
72 "File {} does not have an extension!",
73 path.to_string_lossy()
74 ))),
75 |e|
76 if IN_EXTENSIONS.iter().any(|&ext| ext == e) {
77 Ok(e)
78 } else {
79 Err(ReadStatError::Other(format!(
80 "Expecting extension sas7bdat or sas7bcat.\nFile {} does not have expected extension!",
81 path.to_string_lossy()
82 )))
83 }
84 )
85 }
86
87 fn validate_path(path: &Path) -> Result<PathBuf, ReadStatError> {
88 let abs_path = std::path::absolute(path)
89 .map_err(|e| ReadStatError::Other(format!("Failed to resolve path: {e}")))?;
90
91 if abs_path.exists() {
92 Ok(abs_path)
93 } else {
94 Err(ReadStatError::Other(format!(
95 "File {} does not exist!",
96 abs_path.to_string_lossy()
97 )))
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
109 fn valid_sas7bdat_extension() {
110 let path = Path::new("/some/file.sas7bdat");
111 assert_eq!(
112 ReadStatPath::validate_in_extension(path).unwrap(),
113 "sas7bdat"
114 );
115 }
116
117 #[test]
118 fn valid_sas7bcat_extension() {
119 let path = Path::new("/some/file.sas7bcat");
120 assert_eq!(
121 ReadStatPath::validate_in_extension(path).unwrap(),
122 "sas7bcat"
123 );
124 }
125
126 #[test]
127 fn invalid_extension() {
128 let path = Path::new("/some/file.csv");
129 assert!(ReadStatPath::validate_in_extension(path).is_err());
130 }
131
132 #[test]
133 fn no_extension() {
134 let path = Path::new("/some/file");
135 assert!(ReadStatPath::validate_in_extension(path).is_err());
136 }
137
138 #[test]
141 fn path_to_cstring_normal() {
142 let path = PathBuf::from("/tmp/test.sas7bdat");
143 let cs = ReadStatPath::path_to_cstring(&path).unwrap();
144 assert_eq!(cs.to_str().unwrap(), "/tmp/test.sas7bdat");
145 }
146}