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 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
189fn 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) }
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 }
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 if let Ok(output) = &bridge_output {
272 write_logs(&pkg.name, &log_file, output)?;
273 }
274
275 match bridge_output {
276 Ok(output) => {
277 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 && output.status.code().unwrap() == 1 && stderr == "__IMPL_DEFAULT"
333 {
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 let bridge_output = String::from_utf8(bridge_output.stdout).into_diagnostic()?;
401
402 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 std::fs::create_dir_all(&tmp_dir).into_diagnostic()?;
656
657 std::env::set_current_dir(&tmp_dir).into_diagnostic()?;
659
660 Ok(tmp_dir)
661 }
662}