pkg_rs/
bridge.rs

1use crate::{DEFAULT_LOG_DIR, DEFAULT_WORKING_DIR, db::Db, input::PkgDeclaration};
2use miette::{Diagnostic, IntoDiagnostic, Result};
3use std::{
4    collections::HashMap,
5    env,
6    fs::OpenOptions,
7    io::Write,
8    path::PathBuf,
9    process::{self, Output},
10};
11use thiserror::Error;
12
13use crate::{Pkg, PkgType, PkgVersion, input};
14
15#[derive(Debug, Clone)]
16struct Bridge {
17    name: String,
18    entry_point: PathBuf,
19}
20
21#[derive(Debug)]
22pub struct BridgeApi {
23    bridges: Vec<Bridge>,
24    db: Db,
25}
26
27#[derive(Debug)]
28pub struct BridgeOutput {
29    version: PkgVersion,
30    pkg_path: PathBuf,
31    pkg_type: PkgType,
32}
33
34#[derive(Debug, PartialEq)]
35pub enum Operation {
36    Install,
37    Update,
38    Remove,
39}
40
41#[derive(Debug)]
42pub enum OperationResult {
43    Installed(Pkg),
44    Updated(Pkg),
45    Removed(bool),
46}
47
48#[derive(Error, Debug, Diagnostic)]
49pub enum BridgeApiError {
50    #[error(transparent)]
51    #[diagnostic(code(bridge::io_error))]
52    IoError(#[from] std::io::Error),
53
54    #[error("Bridge not found: {0}")]
55    #[diagnostic(code(bridge::bridge_not_found))]
56    BridgeNotFound(String),
57
58    #[error("Bridge set not found: {0}")]
59    #[diagnostic(code(bridge::bridge_not_found))]
60    BridgeSetNotFound(PathBuf),
61
62    #[error("Bridge set not found: {0}")]
63    #[diagnostic(
64        code(bridge::bridge_not_found),
65        help(
66            "The bridge set path should be a directory that contains bridges (directories that contains executable scripts)"
67        )
68    )]
69    BridgeSetPathAreNotADirectory(PathBuf),
70
71    #[error("Bridge returned an error: {0}")]
72    #[diagnostic(code(bridge::bridge_error))]
73    BridgeError(String),
74
75    #[error("Bridge entry point is not executable: {0}")]
76    #[diagnostic(
77        code(bridge::bridge_entry_point_not_executable),
78        help("Try: `chmod +x <entry_point>`")
79    )]
80    BridgeEntryPointNotExecutable(PathBuf),
81
82    #[error("Bridge returned a wrong output: {0}")]
83    #[diagnostic(
84        code(bridge::bridge_wrong_output),
85        help(
86            "Bridge output should be a new line separated list of three elements: pkg_path,pkg_version,pkg_entry_point(if pkg type is 'Directory')"
87        )
88    )]
89    BridgeWrongOutput(String),
90
91    #[error("Bridge failed at runtime, error: {0}")]
92    #[diagnostic(code(bridge::bridge_failed))]
93    BridgeFailedAtRuntime(String),
94
95    #[error("Bridge returned a wrong version format: {0}")]
96    #[diagnostic(
97        code(bridge::bridge_wrong_version_format),
98        help(
99            "Version format should be three integers (can be strings but not recommended) separated by a dot '.'"
100        )
101    )]
102    BridgeWrongVersionFormat(String),
103
104    #[error("Bridge returned not valid path: {0}")]
105    #[diagnostic(code(bridge::bridge_wrong_path))]
106    BridgeNotValid(PathBuf),
107
108    #[error("Bridge returned not valid entry point: {0}")]
109    #[diagnostic(code(bridge::bridge_wrong_entry_point))]
110    BridgeNotValidEntryPoint(PathBuf),
111
112    #[error("Failed to create log file: {0}")]
113    #[diagnostic(code(bridge::bridge_failed_to_create_log_file))]
114    BridgeFailedToCreateLogFile(String),
115
116    #[error("Failed to open log file: {0}")]
117    #[diagnostic(code(bridge::bridge_failed_to_open_log_file))]
118    BridgeFailedToOpenLogFile(String),
119
120    #[error("Pkg'path with try directory should be a directory: {0}")]
121    #[diagnostic(code(bridge::bridge_entry_point_is_file))]
122    PkgPathWithTryDirectoryShouldBeADirectory(PathBuf),
123
124    #[error("The entry point is a directory: {0}")]
125    #[diagnostic(code(bridge::bridge_entry_point_is_directory))]
126    PkgEntryPointIsDirectory(PathBuf),
127
128    #[error("The entry point is an executable: {0}")]
129    #[diagnostic(code(bridge::bridge_entry_point_is_executable))]
130    PkgEntryPointIsNotExecutable(PathBuf),
131
132    #[error("The pkg path is not executable and type is single executable: {0}")]
133    #[diagnostic(code(bridge::PkgIsNotExecutableWithTypeSingleExecutable))]
134    PkgIsNotExecutableWithTypeSingleExecutable(PathBuf),
135
136    #[error("The pkg path should be a file if type is single executable: {0}")]
137    #[diagnostic(code(bridge::PkgPathWithTrySingleExecutableShouldBeFile))]
138    PkgPathWithTrySingleExecutableShouldBeFile(PathBuf),
139}
140
141fn write_logs(pkg_name: &str, log_file: &PathBuf, bridge_output: &Output) -> Result<()> {
142    let mut log_file_handle = OpenOptions::new()
143        .create(true)
144        .append(true)
145        .open(log_file)
146        .map_err(|err| BridgeApiError::BridgeFailedToOpenLogFile(err.to_string()))?;
147
148    // Write stdout to log
149    log_file_handle
150        .write_all(format!("\n|PKG={}|:::::::\n", &pkg_name).as_bytes())
151        .into_diagnostic()?;
152    log_file_handle
153        .write_all("|STDOUT|::::::::\n".as_bytes())
154        .into_diagnostic()?;
155    log_file_handle
156        .write_all(&bridge_output.stdout)
157        .into_diagnostic()?;
158    log_file_handle.write_all(b"\n").into_diagnostic()?;
159    log_file_handle
160        .write_all("\n|STDERR|::::::::\n".as_bytes())
161        .into_diagnostic()?;
162    log_file_handle
163        .write_all(&bridge_output.stderr)
164        .into_diagnostic()?;
165    log_file_handle.write_all(b"\n").into_diagnostic()?;
166
167    Ok(())
168}
169
170mod default_impls {
171    use std::path::PathBuf;
172
173    use miette::{IntoDiagnostic, Result};
174    pub fn remove() -> Result<bool> {
175        let pkg_path = std::env::var("pkg_path").unwrap();
176        let mut removed = false;
177        if PathBuf::from(&pkg_path).exists() {
178            if PathBuf::from(&pkg_path).is_dir() {
179                std::fs::remove_dir_all(&pkg_path).into_diagnostic()?;
180            } else {
181                std::fs::remove_file(&pkg_path).into_diagnostic()?;
182            }
183            removed = true;
184        }
185        Ok(removed)
186    }
187}
188
189// NOTE: unix only
190fn is_executable(path: &PathBuf) -> Result<bool> {
191    use std::os::unix::fs::PermissionsExt;
192
193    let metadata = path.metadata().into_diagnostic()?;
194    let permissions = metadata.permissions();
195    Ok(permissions.mode() & 0o111 != 0) // Check if any execute bit is set
196}
197
198impl Operation {
199    pub fn display(&self) -> String {
200        match self {
201            Operation::Install => "install".to_string(),
202            Operation::Update => "update".to_string(),
203            Operation::Remove => "remove".to_string(),
204        }
205    }
206}
207
208impl BridgeApi {
209    pub fn new(
210        bridge_set_path: PathBuf,
211        needed_bridges: &[String],
212        db_path: &PathBuf,
213    ) -> Result<Self> {
214        let bridges = Self::load_bridges(&bridge_set_path, needed_bridges)?;
215
216        let db = Db::new(db_path)?;
217
218        Ok(Self { bridges, db })
219    }
220
221    pub fn run_operation(
222        &self,
223        bridge_name: &str,
224        pkg: &PkgDeclaration,
225        operation: Operation,
226    ) -> Result<Option<Pkg>> {
227        let bridge_entry_point = &self
228            .bridges
229            .iter()
230            .find(|b| b.name == bridge_name)
231            .ok_or(BridgeApiError::BridgeNotFound(bridge_name.to_string()))?
232            .entry_point;
233
234        Self::setup_working_directory(bridge_name, &pkg.name)?;
235
236        let input = pkg.input.to_string();
237        let attributes = &pkg.attributes;
238
239        let log_file = PathBuf::from(format!("{}/{}.log", &DEFAULT_LOG_DIR, &bridge_name));
240
241        let log_file_parent = log_file.parent().unwrap();
242        let _ = std::fs::create_dir_all(log_file_parent)
243            .map_err(|err| BridgeApiError::BridgeFailedToCreateLogFile(err.to_string()));
244        if !log_file.exists() {
245            std::fs::File::create(&log_file)
246                .map_err(|err| BridgeApiError::BridgeFailedToCreateLogFile(err.to_string()))?;
247        }
248
249        let mut pkg_path = None;
250
251        if (operation == Operation::Update) || (operation == Operation::Remove) {
252            pkg_path = self
253                .db
254                .get_pkgs_by_name(std::slice::from_ref(&pkg.name))?
255                .first()
256                .map(|p| p.path.clone());
257            // NOTE: this is good to do not break if
258            // some thing is wrong or db is manually modified, but it's not returned
259            // the correct result
260        }
261
262        Self::pass_opts_to_env(attributes, pkg_path, &log_file.to_string_lossy())?;
263
264        let mut bridge = process::Command::new(bridge_entry_point);
265        bridge.arg(operation.display());
266        bridge.arg(input.clone());
267
268        let bridge_output = bridge.output();
269
270        // Write the log
271        if let Ok(output) = &bridge_output {
272            write_logs(&pkg.name, &log_file, output)?;
273        }
274
275        match bridge_output {
276            Ok(output) => {
277                // Bridge command succeeded
278                let res = match operation {
279                    Operation::Install => {
280                        let parsed_output = Self::parse_bridge_output(output)?;
281                        let pkg = Pkg {
282                            name: pkg.name.clone(),
283                            version: parsed_output.version,
284                            path: parsed_output.pkg_path,
285                            pkg_type: parsed_output.pkg_type,
286                        };
287                        Ok(Some(pkg))
288                    }
289                    Operation::Update => {
290                        let success = output.status.success();
291                        let stderr = String::from_utf8(output.stderr.clone()).into_diagnostic()?;
292                        let stderr = stderr.trim();
293
294                        let output = if !success
295                            && output.status.code().unwrap() == 1
296                            && stderr == "__IMPL_DEFAULT"
297                        {
298                            let output = process::Command::new(bridge_entry_point)
299                                .arg(Operation::Install.display())
300                                .arg(input.clone())
301                                .output();
302
303                            if let Ok(bridge_output) = &output {
304                                write_logs(&pkg.name, &log_file, bridge_output)?;
305
306                                if bridge_output.status.success() {
307                                    let _ = default_impls::remove()?;
308                                }
309                            }
310
311                            output.into_diagnostic()?
312                        } else {
313                            output
314                        };
315
316                        let parsed_output = Self::parse_bridge_output(output)?;
317                        let pkg = Pkg {
318                            name: pkg.name.clone(),
319                            version: parsed_output.version,
320                            path: parsed_output.pkg_path,
321                            pkg_type: parsed_output.pkg_type,
322                        };
323                        Ok(Some(pkg))
324                    }
325                    Operation::Remove => {
326                        let success = output.status.success();
327                        let stderr = String::from_utf8(output.stderr).into_diagnostic()?;
328                        let stderr = stderr.trim();
329
330                        if !success // if it failed
331                            && output.status.code().unwrap() == 1 // and return 1
332                            && stderr == "__IMPL_DEFAULT"
333                        // and print the the
334                        // stderr __IMPL_DEFAULT
335                        // a log right
336                        {
337                            default_impls::remove()?;
338                        } else {
339                            return Err(BridgeApiError::BridgeError(stderr.to_string()).into());
340                        }
341                        Ok(None)
342                    }
343                };
344
345                Self::clear_env(&attributes.keys().map(|s| s.to_string()).collect())?;
346
347                res
348            }
349            Err(err) => {
350                Self::clear_env(&attributes.keys().map(|s| s.to_string()).collect())?;
351
352                Err(BridgeApiError::BridgeFailedAtRuntime(err.to_string()).into())
353            }
354        }
355    }
356
357    pub fn install(&self, bridge_name: &str, pkg: &PkgDeclaration) -> Result<Pkg> {
358        self.run_operation(bridge_name, pkg, Operation::Install)
359            .map(|p| p.unwrap())
360    }
361
362    pub fn update(&self, bridge_name: &str, pkg: &PkgDeclaration) -> Result<Pkg> {
363        self.run_operation(bridge_name, pkg, Operation::Update)
364            .map(|p| p.unwrap())
365    }
366
367    pub fn remove(&self, bridge_name: &str, pkg: &PkgDeclaration) -> Result<bool> {
368        let res = self.run_operation(bridge_name, pkg, Operation::Remove)?;
369        Ok(res.is_none())
370    }
371
372    pub fn default_impls_remove(&self, pkg_name: &str) -> Result<bool> {
373        let pkg_path = self
374            .db
375            .get_pkgs_by_name(std::slice::from_ref(&pkg_name.to_string()))?
376            .first()
377            .expect("Failed to get pkg from db, can't remove it")
378            .path
379            .clone();
380        unsafe {
381            std::env::set_var("pkg_path", pkg_path);
382        }
383        use default_impls::remove;
384
385        remove()
386    }
387
388    fn parse_bridge_output(bridge_output: Output) -> Result<BridgeOutput> {
389        const BRIDGE_OUTPUT_SEPARATOR: char = ',';
390        const VERSION_SEPARATOR: char = '.';
391
392        if !bridge_output.status.success() {
393            return Err(BridgeApiError::BridgeError(
394                String::from_utf8(bridge_output.stderr)
395                    .unwrap_or("failed to parse bridge output".to_string()),
396            ))?;
397        }
398
399        // to string
400        let bridge_output = String::from_utf8(bridge_output.stdout).into_diagnostic()?;
401
402        // get the first line of the bridge output
403        let first_line =
404            bridge_output
405                .lines()
406                .next()
407                .ok_or(BridgeApiError::IoError(std::io::Error::other(
408                    "Wrong bridge output, no thing is returned",
409                )))?;
410
411        let first_line = first_line.trim();
412
413        let split = first_line
414            .split(BRIDGE_OUTPUT_SEPARATOR)
415            .collect::<Vec<&str>>();
416
417        let pkg_path;
418        let version;
419        let pkg_type;
420
421        if split.len() > 3 || split.len() < 2 {
422            return Err(BridgeApiError::BridgeWrongOutput(bridge_output))?;
423        } else {
424            pkg_path = PathBuf::from(split.first().unwrap().to_string());
425            let version_str = split.get(1).unwrap().to_string();
426            pkg_type = match split.get(2) {
427                Some(entry_point) => PkgType::Directory(PathBuf::from(entry_point)),
428                None => PkgType::SingleExecutable,
429            };
430
431            let version_split = version_str.split(VERSION_SEPARATOR).collect::<Vec<&str>>();
432
433            if version_split.len() != 3 {
434                return Err(BridgeApiError::BridgeWrongVersionFormat(version_str))?;
435            } else {
436                version = PkgVersion {
437                    first_cell: version_split[0].to_string(),
438                    second_cell: version_split[1].to_string(),
439                    third_cell: version_split[2].to_string(),
440                };
441            }
442        }
443
444        let pwd = std::env::current_dir().into_diagnostic()?;
445
446        let pkg_path = if pkg_path.is_relative() {
447            pwd.join(pkg_path)
448        } else {
449            pkg_path
450        };
451
452        let pkg_type = match pkg_type {
453            PkgType::Directory(path) => {
454                let path = if path.is_relative() {
455                    pwd.join(path)
456                } else {
457                    path
458                };
459                PkgType::Directory(path)
460            }
461            _ => pkg_type,
462        };
463
464        if !pkg_path.exists() {
465            return Err(BridgeApiError::BridgeNotValid(pkg_path))?;
466        }
467
468        if let PkgType::SingleExecutable = &pkg_type {
469            if !pkg_path.is_file() {
470                return Err(BridgeApiError::PkgPathWithTrySingleExecutableShouldBeFile(
471                    pkg_path.clone(),
472                ))?;
473            }
474
475            if !is_executable(&pkg_path)? {
476                return Err(BridgeApiError::PkgIsNotExecutableWithTypeSingleExecutable(
477                    pkg_path.clone(),
478                ))?;
479            }
480        }
481
482        if let PkgType::Directory(path) = &pkg_type
483            && !path.exists()
484        {
485            return Err(BridgeApiError::BridgeNotValidEntryPoint(path.clone()))?;
486        }
487
488        if let PkgType::Directory(_) = &pkg_type
489            && !pkg_path.is_dir()
490        {
491            return Err(BridgeApiError::PkgPathWithTryDirectoryShouldBeADirectory(
492                pkg_path.clone(),
493            ))?;
494        }
495
496        if let PkgType::Directory(path) = &pkg_type
497            && path.is_dir()
498        {
499            return Err(BridgeApiError::PkgEntryPointIsDirectory(path.clone()))?;
500        }
501
502        if let PkgType::Directory(path) = &pkg_type
503            && !is_executable(path)?
504        {
505            return Err(BridgeApiError::PkgEntryPointIsNotExecutable(path.clone()))?;
506        }
507
508        Ok(BridgeOutput {
509            version,
510            pkg_path,
511            pkg_type,
512        })
513    }
514
515    fn load_bridges(bridge_set_path: &PathBuf, needed_bridges: &[String]) -> Result<Vec<Bridge>> {
516        const BRIDGE_ENTRY_POINT_NAME: &str = "run";
517
518        if !bridge_set_path.exists() {
519            return Err(BridgeApiError::BridgeSetNotFound(bridge_set_path.clone()).into());
520        };
521
522        if !bridge_set_path.is_dir() {
523            return Err(
524                BridgeApiError::BridgeSetPathAreNotADirectory(bridge_set_path.clone()).into(),
525            );
526        }
527
528        let content = bridge_set_path
529            .read_dir()
530            .map_err(BridgeApiError::IoError)?;
531
532        let mut bridges = Vec::<Bridge>::new();
533
534        for file in content {
535            let file = file.map_err(BridgeApiError::IoError)?;
536
537            if file.file_type().map_err(BridgeApiError::IoError)?.is_dir() {
538                let bridge_dir = file.path();
539                let bridge_name = bridge_dir
540                    .file_stem()
541                    .unwrap()
542                    .to_str()
543                    .unwrap()
544                    .to_string();
545
546                if !needed_bridges.contains(&bridge_name) {
547                    continue;
548                }
549
550                let entry_point_path = bridge_dir.join(BRIDGE_ENTRY_POINT_NAME);
551                if entry_point_path.exists() && entry_point_path.is_file() {
552                    if !is_executable(&entry_point_path)? {
553                        Err(BridgeApiError::BridgeEntryPointNotExecutable(
554                            entry_point_path.clone(),
555                        ))?;
556                    }
557
558                    bridges.push(Bridge {
559                        name: bridge_name,
560                        entry_point: entry_point_path,
561                    });
562                }
563            }
564        }
565
566        let missing_bridges = needed_bridges
567            .iter()
568            .filter(|b| !bridges.iter().any(|bridge| &bridge.name == *b))
569            .cloned()
570            .collect::<Vec<String>>();
571
572        if !missing_bridges.is_empty() {
573            return Err(BridgeApiError::BridgeNotFound(
574                missing_bridges.first().unwrap().to_string(),
575            )
576            .into());
577        }
578
579        Ok(bridges)
580    }
581
582    fn pass_opts_to_env(
583        attributes: &HashMap<String, input::AttributeValue>,
584        pkg_path: Option<PathBuf>,
585        log_file: &str,
586    ) -> Result<(), BridgeApiError> {
587        unsafe {
588            if let Some(path) = pkg_path {
589                if env::var("pkg_path").is_ok() {
590                    env::remove_var("pkg_path");
591                }
592                env::set_var("pkg_path", path);
593            }
594
595            if env::var("pkg_log_file").is_ok() {
596                env::remove_var("pkg_log_file");
597            }
598            env::set_var("pkg_log_file", log_file);
599        }
600
601        for (key, value) in attributes {
602            let value = match value {
603                input::AttributeValue::String(value) => value.to_string(),
604                input::AttributeValue::Integer(value) => value.to_string(),
605                input::AttributeValue::Float(value) => value.to_string(),
606                input::AttributeValue::Boolean(value) => value.to_string(),
607            };
608
609            if env::var(key).is_ok() {
610                unsafe {
611                    env::remove_var(key);
612                }
613            }
614
615            unsafe {
616                env::set_var(key, value);
617            }
618        }
619
620        Ok(())
621    }
622
623    fn clear_env(attributes_keys: &Vec<String>) -> Result<()> {
624        for key in attributes_keys {
625            if env::var(key).is_ok() {
626                unsafe {
627                    env::remove_var(key);
628                }
629            }
630        }
631        Ok(())
632    }
633
634    fn setup_working_directory(bridge_name: &str, pkg_name: &str) -> Result<PathBuf> {
635        use std::time::{SystemTime, UNIX_EPOCH};
636
637        let tmp_dir_base = PathBuf::from(DEFAULT_WORKING_DIR)
638            .join(bridge_name)
639            .join(pkg_name);
640
641        let tmp_dir = loop {
642            let timestamp = SystemTime::now()
643                .duration_since(UNIX_EPOCH)
644                .unwrap()
645                .as_nanos();
646
647            let tmp_dir = tmp_dir_base.join(format!("{timestamp}"));
648
649            if !tmp_dir.exists() {
650                break tmp_dir;
651            }
652        };
653
654        // Create the directory
655        std::fs::create_dir_all(&tmp_dir).into_diagnostic()?;
656
657        // Change to the directory
658        std::env::set_current_dir(&tmp_dir).into_diagnostic()?;
659
660        Ok(tmp_dir)
661    }
662}