Skip to main content

txtx_addon_kit/helpers/
fs.rs

1use serde::ser::{Serialize, SerializeMap, Serializer};
2use std::borrow::BorrowMut;
3use std::fmt::{self, Display, Formatter};
4use std::fs;
5use std::fs::File;
6use std::hash::{Hash, Hasher};
7use std::io::Write;
8use std::path::Path;
9use std::str::FromStr;
10use std::{collections::HashMap, future::Future, path::PathBuf, pin::Pin};
11use url::Url;
12
13pub type FileAccessorResult<T> = Pin<Box<dyn Future<Output = Result<T, String>>>>;
14
15pub trait FileAccessor {
16    fn file_exists(&self, path: String) -> FileAccessorResult<bool>;
17    fn read_file(&self, path: String) -> FileAccessorResult<String>;
18    fn read_contracts_content(
19        &self,
20        contracts_paths: Vec<String>,
21    ) -> FileAccessorResult<HashMap<String, String>>;
22    fn write_file(&self, path: String, content: &[u8]) -> FileAccessorResult<()>;
23}
24
25#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
26#[serde(untagged)]
27pub enum FileLocation {
28    FileSystem { path: PathBuf },
29    Url { url: Url },
30}
31
32impl Hash for FileLocation {
33    fn hash<H: Hasher>(&self, state: &mut H) {
34        match self {
35            Self::FileSystem { path } => {
36                let canonicalized_path = path.canonicalize().unwrap_or(path.clone());
37                canonicalized_path.hash(state);
38            }
39            Self::Url { url } => {
40                url.hash(state);
41            }
42        }
43    }
44}
45
46impl Display for FileLocation {
47    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
48        match self {
49            FileLocation::FileSystem { path } => write!(f, "{}", path.display()),
50            FileLocation::Url { url } => write!(f, "{}", url),
51        }
52    }
53}
54
55impl FileLocation {
56    pub fn try_parse(
57        location_string: &str,
58        workspace_root_location_hint: Option<&FileLocation>,
59    ) -> Option<FileLocation> {
60        if let Ok(location) = FileLocation::from_url_string(location_string) {
61            return Some(location);
62        }
63        if let Ok(FileLocation::FileSystem { path }) =
64            FileLocation::from_path_string(location_string)
65        {
66            match (workspace_root_location_hint, path.is_relative()) {
67                (None, true) => return None,
68                (Some(hint), true) => {
69                    let mut location = hint.clone();
70                    location.append_path(location_string).ok()?;
71                    return Some(location);
72                }
73                (_, false) => return Some(FileLocation::FileSystem { path }),
74            }
75        }
76        None
77    }
78
79    pub fn from_path(path: PathBuf) -> FileLocation {
80        FileLocation::FileSystem { path }
81    }
82
83    pub fn from_url(url: Url) -> FileLocation {
84        FileLocation::Url { url }
85    }
86
87    pub fn from_url_string(url_string: &str) -> Result<FileLocation, String> {
88        let url = Url::from_str(url_string)
89            .map_err(|e| format!("unable to parse {} as a url\n{:?}", url_string, e))?;
90
91        #[cfg(not(feature = "wasm"))]
92        if url.scheme() == "file" {
93            let path =
94                url.to_file_path().map_err(|_| format!("unable to conver url {} to path", url))?;
95            return Ok(FileLocation::FileSystem { path });
96        }
97
98        Ok(FileLocation::Url { url })
99    }
100
101    pub fn working_dir() -> FileLocation {
102        FileLocation::from_path_string(".").unwrap()
103    }
104
105    pub fn from_path_string(path_string: &str) -> Result<FileLocation, String> {
106        let path = PathBuf::from_str(path_string)
107            .map_err(|e| format!("unable to parse {} as a path\n{:?}", path_string, e))?;
108        Ok(FileLocation::FileSystem { path })
109    }
110
111    pub fn append_path(&mut self, path_string: &str) -> Result<(), String> {
112        let path_to_append = PathBuf::from_str(path_string)
113            .map_err(|e| format!("unable to read relative path {}\n{:?}", path_string, e))?;
114        match self.borrow_mut() {
115            FileLocation::FileSystem { path } => {
116                path.extend(&path_to_append);
117            }
118            FileLocation::Url { url } => {
119                let mut paths_segments =
120                    url.path_segments_mut().map_err(|_| "unable to mutate url".to_string())?;
121                for component in path_to_append.components() {
122                    let segment = component
123                        .as_os_str()
124                        .to_str()
125                        .ok_or(format!("unable to format component {:?}", component))?;
126                    paths_segments.push(segment);
127                }
128            }
129        }
130        Ok(())
131    }
132
133    pub fn expect_path_buf(&self) -> PathBuf {
134        match self {
135            FileLocation::FileSystem { path } => path.clone(),
136            FileLocation::Url { .. } => unreachable!(),
137        }
138    }
139
140    pub fn read_content_as_utf8(&self) -> Result<String, String> {
141        let content = self.read_content()?;
142        let contract_as_utf8 = String::from_utf8(content).map_err(|e| {
143            format!("unable to read content from {} as utf8 ({})", self, e.to_string())
144        })?;
145        Ok(contract_as_utf8)
146    }
147
148    fn fs_read_content(path: &Path) -> Result<Vec<u8>, String> {
149        use std::fs::File;
150        use std::io::{BufReader, Read};
151        let file = File::open(path)
152            .map_err(|e| format!("unable to read file {} ({})", path.display(), e.to_string()))?;
153        let mut file_reader = BufReader::new(file);
154        let mut file_buffer = vec![];
155        file_reader
156            .read_to_end(&mut file_buffer)
157            .map_err(|e| format!("unable to read file {} ({})", path.display(), e.to_string()))?;
158        Ok(file_buffer)
159    }
160
161    fn fs_exists(path: &Path) -> bool {
162        path.exists()
163    }
164    fn fs_write_content(file_path: &PathBuf, content: &[u8]) -> Result<(), String> {
165        let mut parent_directory = file_path.clone();
166        parent_directory.pop();
167        fs::create_dir_all(&parent_directory).map_err(|e| {
168            format!("unable to create parent directory {}\n{}", parent_directory.display(), e)
169        })?;
170        let mut file = File::create(file_path)
171            .map_err(|e| format!("unable to open file {}\n{}", file_path.display(), e))?;
172        file.write_all(content)
173            .map_err(|e| format!("unable to write file {}\n{}", file_path.display(), e))?;
174        Ok(())
175    }
176
177    fn fs_create_dir_all(path: &Path) -> Result<(), String> {
178        fs::create_dir_all(path).map_err(|e| {
179            format!("unable to create directory {}\n{}", path.display(), e.to_string())
180        })
181    }
182
183    fn fs_create_file_with_dirs(file_path: &PathBuf) -> Result<(), String> {
184        let mut parent_directory = file_path.clone();
185        parent_directory.pop();
186        if !parent_directory.exists() {
187            fs::create_dir_all(&parent_directory).map_err(|e| {
188                format!("unable to create parent directory {}\n{}", parent_directory.display(), e)
189            })?;
190        }
191        let _ = File::create(file_path)
192            .map_err(|e| format!("unable to open file {}\n{}", file_path.display(), e))?;
193
194        Ok(())
195    }
196
197    pub fn get_workspace_root_location(&self) -> Result<FileLocation, String> {
198        let mut workspace_root_location = self.clone();
199        match workspace_root_location.borrow_mut() {
200            FileLocation::FileSystem { path } => {
201                let mut manifest_found = false;
202                while path.pop() {
203                    path.push("txtx.yml");
204                    if FileLocation::fs_exists(path) {
205                        path.pop();
206                        manifest_found = true;
207                        break;
208                    }
209                    path.pop();
210                }
211
212                match manifest_found {
213                    true => Ok(workspace_root_location),
214                    false => Err(format!("unable to find root location from {}", self)),
215                }
216            }
217            _ => {
218                unimplemented!();
219            }
220        }
221    }
222
223    pub async fn get_workspace_manifest_location(
224        &self,
225        file_accessor: Option<&dyn FileAccessor>,
226    ) -> Result<FileLocation, String> {
227        match file_accessor {
228            None => {
229                let mut project_root_location = self.get_workspace_root_location()?;
230                project_root_location.append_path("txtx.yml")?;
231                Ok(project_root_location)
232            }
233            Some(file_accessor) => {
234                let mut manifest_location = None;
235                let mut parent_location = self.get_parent_location();
236                while let Ok(ref parent) = parent_location {
237                    let mut candidate = parent.clone();
238                    candidate.append_path("txtx.yml")?;
239
240                    if let Ok(exists) = file_accessor.file_exists(candidate.to_string()).await {
241                        if exists {
242                            manifest_location = Some(candidate);
243                            break;
244                        }
245                    }
246                    if &parent.get_parent_location().unwrap() == parent {
247                        break;
248                    }
249                    parent_location = parent.get_parent_location();
250                }
251                match manifest_location {
252                    Some(manifest_location) => Ok(manifest_location),
253                    None => Err(format!(
254                        "No Clarinet.toml is associated to the contract {}",
255                        self.get_file_name().unwrap_or_default()
256                    )),
257                }
258            }
259        }
260    }
261
262    pub fn get_absolute_path(&self) -> Result<PathBuf, String> {
263        match self {
264            FileLocation::FileSystem { path } => {
265                let abs = fs::canonicalize(path)
266                    .map_err(|e| format!("failed to get absolute path: {e}"))?;
267                Ok(abs)
268            }
269            FileLocation::Url { url } => {
270                return Err(format!("cannot get absolute path for url {}", url))
271            }
272        }
273    }
274
275    pub fn get_parent_location(&self) -> Result<FileLocation, String> {
276        let mut parent_location = self.clone();
277        match &mut parent_location {
278            FileLocation::FileSystem { path } => {
279                let mut parent = path.clone();
280                parent.pop();
281                if parent.to_str() == path.to_str() {
282                    return Err(String::from("reached root"));
283                }
284                path.pop();
285            }
286            FileLocation::Url { url } => {
287                let mut segments =
288                    url.path_segments_mut().map_err(|_| "unable to mutate url".to_string())?;
289                segments.pop();
290            }
291        }
292        Ok(parent_location)
293    }
294
295    pub fn get_relative_path_from_base(
296        &self,
297        base_location: &FileLocation,
298    ) -> Result<String, String> {
299        let file = self.to_string();
300        Ok(file[(base_location.to_string().len() + 1)..].to_string())
301    }
302
303    pub fn get_relative_location(&self) -> Result<String, String> {
304        let base = self.get_workspace_root_location().map(|l| l.to_string())?;
305        let file = self.to_string();
306        let offset = if base.is_empty() { 0 } else { 1 };
307        Ok(file[(base.len() + offset)..].to_string())
308    }
309
310    pub fn get_file_name(&self) -> Option<String> {
311        match self {
312            FileLocation::FileSystem { path } => {
313                path.file_name().and_then(|f| Some(f.to_str()?.to_string()))
314            }
315            FileLocation::Url { url } => {
316                url.path_segments().and_then(|p| Some(p.last()?.to_string()))
317            }
318        }
319    }
320}
321
322impl FileLocation {
323    pub fn read_content(&self) -> Result<Vec<u8>, String> {
324        let bytes = match &self {
325            FileLocation::FileSystem { path } => FileLocation::fs_read_content(path),
326            FileLocation::Url { url } => match url.scheme() {
327                #[cfg(not(feature = "wasm"))]
328                "file" => {
329                    let path = url
330                        .to_file_path()
331                        .map_err(|e| format!("unable to convert url {} to path\n{:?}", url, e))?;
332                    FileLocation::fs_read_content(&path)
333                }
334                "http" | "https" => {
335                    unimplemented!()
336                }
337                _ => {
338                    unimplemented!()
339                }
340            },
341        }?;
342        Ok(bytes)
343    }
344
345    pub fn exists(&self) -> bool {
346        match self {
347            FileLocation::FileSystem { path } => FileLocation::fs_exists(path),
348            FileLocation::Url { url: _url } => unimplemented!(),
349        }
350    }
351
352    pub fn write_content(&self, content: &[u8]) -> Result<(), String> {
353        match self {
354            FileLocation::FileSystem { path } => FileLocation::fs_write_content(path, content),
355            FileLocation::Url { url: _url } => unimplemented!(),
356        }
357    }
358
359    pub fn create_dir_all(&self) -> Result<(), String> {
360        match self {
361            FileLocation::FileSystem { path } => FileLocation::fs_create_dir_all(path),
362            FileLocation::Url { url: _url } => Ok(()),
363        }
364    }
365
366    pub fn create_dir_and_file(&self) -> Result<(), String> {
367        match self {
368            FileLocation::FileSystem { path } => FileLocation::fs_create_file_with_dirs(path),
369            FileLocation::Url { .. } => Ok(()),
370        }
371    }
372
373    pub fn to_url_string(&self) -> Result<String, String> {
374        match self {
375            #[cfg(not(feature = "wasm"))]
376            FileLocation::FileSystem { path } => {
377                let file_path = self.to_string();
378                let url = Url::from_file_path(file_path)
379                    .map_err(|_| format!("unable to conver path {} to url", path.display()))?;
380                Ok(url.to_string())
381            }
382            FileLocation::Url { url } => Ok(url.to_string()),
383            #[allow(unreachable_patterns)]
384            _ => unreachable!(),
385        }
386    }
387}
388
389impl Serialize for FileLocation {
390    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
391    where
392        S: Serializer,
393    {
394        let mut map = serializer.serialize_map(Some(1))?;
395        match self {
396            FileLocation::FileSystem { path: _ } => {
397                let path = match self.get_relative_location() {
398                    Ok(relative_path) => relative_path, // Use relative path if possible
399                    Err(_) => self.to_string(),         // Fallback on fully qualified path
400                };
401                map.serialize_entry("path", &path)?;
402            }
403            FileLocation::Url { url } => {
404                map.serialize_entry("url", &url.to_string())?;
405            }
406        }
407        map.end()
408    }
409}
410
411pub fn get_manifest_location(path: Option<String>) -> Option<FileLocation> {
412    if let Some(path) = path {
413        let manifest_path = PathBuf::from(path);
414        if !manifest_path.exists() {
415            return None;
416        }
417        Some(FileLocation::from_path(manifest_path))
418    } else {
419        let mut current_dir = std::env::current_dir().unwrap();
420        loop {
421            current_dir.push("txtx.yml");
422
423            if current_dir.exists() {
424                return Some(FileLocation::from_path(current_dir));
425            }
426            current_dir.pop();
427
428            if !current_dir.pop() {
429                return None;
430            }
431        }
432    }
433}
434
435pub fn get_txtx_files_paths(
436    dir: &str,
437    environment_selector: &Option<String>,
438) -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
439    let dir = std::fs::read_dir(dir)?;
440    let mut files_paths = vec![];
441    for res in dir.into_iter() {
442        let Ok(dir_entry) = res else {
443            continue;
444        };
445        let path = dir_entry.path();
446
447        // if our path is a file with an extension
448        if let Some(ext) = path.extension() {
449            // and that extension is either "tx" or "txvars"
450            if ["tx", "txvars"].contains(&ext.to_str().unwrap()) {
451                // and it has a filename (always true if we have an extension)
452                if let Some(file_name) = path.file_name() {
453                    let comps = file_name.to_str().unwrap().split(".").collect::<Vec<_>>();
454                    // if it has more than two components
455                    if comps.len() > 2 {
456                        // then we require that the second component match the environment (i.e. signers.devnet.tx)
457                        let Some(env) = environment_selector else {
458                            continue;
459                        };
460                        if comps[comps.len() - 2].eq(env) {
461                            files_paths.push(path);
462                        }
463                    // it it doesn't have more than two components, include the file
464                    } else {
465                        files_paths.push(path);
466                    }
467                }
468            }
469        }
470        // otherwise, if the path is a directory
471        else if path.is_dir() {
472            let component = path.components().last().expect("dir has no components");
473            if let Some(folder) = component.as_os_str().to_str() {
474                // and that directory's top folder matches the env (such as devnet/signers.tx)
475                if let Some(env) = environment_selector {
476                    if folder.eq(env) {
477                        // then we recurse into that directory
478                        let mut sub_files_paths = get_txtx_files_paths(
479                            &path.to_str().expect("couldn't turn path back to string"),
480                            environment_selector,
481                        )?;
482                        files_paths.append(&mut sub_files_paths);
483                    }
484                }
485            }
486        }
487    }
488
489    Ok(files_paths)
490}
491
492pub fn get_path_from_components(comps: Vec<&str>) -> String {
493    let mut path = PathBuf::new();
494    for comp in comps {
495        path.push(comp);
496    }
497    path.display().to_string()
498}