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, Err(_) => self.to_string(), };
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 let Some(ext) = path.extension() {
449 if ["tx", "txvars"].contains(&ext.to_str().unwrap()) {
451 if let Some(file_name) = path.file_name() {
453 let comps = file_name.to_str().unwrap().split(".").collect::<Vec<_>>();
454 if comps.len() > 2 {
456 let Some(env) = environment_selector else {
458 continue;
459 };
460 if comps[comps.len() - 2].eq(env) {
461 files_paths.push(path);
462 }
463 } else {
465 files_paths.push(path);
466 }
467 }
468 }
469 }
470 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 if let Some(env) = environment_selector {
476 if folder.eq(env) {
477 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}