1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt::Display,
4 path::{Path, PathBuf},
5 str::FromStr,
6};
7
8use anyhow::bail;
9use base64::Engine;
10use hash_value::{to_value, Value};
11use serde::{de::Error as DeError, ser::Error as SerError, Deserialize, Serialize};
12use strum_macros::{Display, EnumIter};
13use url::Url;
14
15use crate::{cache::FileChangesMatcher, error::Error, unit_enum_deserialize, unit_enum_from_str};
16
17#[derive(Hash, PartialEq, Eq, Serialize, EnumIter, Display, Debug, Clone, Copy)]
18pub enum ExecutorKind {
19 Rust,
20 Node,
21}
22
23unit_enum_from_str!(ExecutorKind);
24unit_enum_deserialize!(ExecutorKind);
25
26#[derive(Serialize, Debug, Hash, PartialEq, Eq, Clone)]
27#[serde(untagged)]
28pub enum ExecutorReference {
29 Standard {
30 url: Url,
31 },
32 Custom {
33 url: Url,
34 #[serde(flatten)]
35 location: Location,
36 },
37}
38
39impl Display for ExecutorReference {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Standard { url } => url.fmt(f),
43 Self::Custom { url, location } => {
44 let fmt_git_location = |url: &Url, options: &GitOptions| {
45 let mut out = url.to_string();
46 if let Some(path) = options.path() {
47 out.push_str(&format!("[/{}]", path.display()));
48 }
49 if let Some(checkout) = options.checkout() {
50 out.push_str(&format!("@{}", checkout));
51 }
52 out
53 };
54
55 match location {
56 Location::Git {
57 options: git_options,
58 }
59 | Location::GitOverHttp { git_options, .. }
60 | Location::GitOverSsh { git_options, .. } => {
61 write!(f, "{}", fmt_git_location(url, git_options))
62 }
63 _ => url.fmt(f),
64 }
65 }
66 }
67 }
68}
69
70const URL_KEY: &str = "url";
71const FORMAT_KEY: &str = "format";
72const AUTHENTICATION_KEY: &str = "authentication";
73
74impl<'de> Deserialize<'de> for ExecutorReference {
75 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76 where
77 D: serde::Deserializer<'de>,
78 {
79 let root = Value::deserialize(deserializer)?;
80 let url = root
81 .at(URL_KEY)
82 .and_then(Value::as_str)
83 .or_else(|| root.as_str())
84 .map(|s| Url::parse(s).map_err(D::Error::custom))
85 .transpose()?
86 .ok_or_else(|| D::Error::missing_field(URL_KEY))?;
87 let is_single_url = matches!(root, Value::String(_));
88
89 match url.scheme() {
90 "std" => Ok(Self::Standard { url }),
91 custom_executor_scheme => Ok(Self::Custom {
92 location: match custom_executor_scheme {
93 "file" => Location::LocalFileSystem {
94 options: if is_single_url {
95 FileSystemOptions::default()
96 } else {
97 FileSystemOptions::deserialize(&root).map_err(D::Error::custom)?
98 },
99 },
100 "http" | "https" => {
101 let format = root
102 .at(FORMAT_KEY)
103 .map(|f| HttpFormatIdentifier::deserialize(f).map_err(D::Error::custom))
104 .transpose()?
105 .ok_or_else(|| D::Error::missing_field(FORMAT_KEY))?;
106
107 let transport =
108 HttpTransport::deserialize(&root).map_err(D::Error::custom)?;
109
110 match format {
111 HttpFormatIdentifier::Git => Location::GitOverHttp {
112 transport,
113 git_options: GitOptions::deserialize(&root)
114 .map_err(D::Error::custom)?,
115 authentication: root
116 .at(AUTHENTICATION_KEY)
117 .map(|auth| {
118 GitPlainAuthentication::deserialize(auth)
119 .map_err(D::Error::custom)
120 })
121 .transpose()?,
122 },
123 HttpFormatIdentifier::Tarball => Location::TarballOverHttp {
124 transport,
125 tarball_options: TarballOptions::deserialize(&root)
126 .map_err(D::Error::custom)?,
127 authentication: root
128 .at(AUTHENTICATION_KEY)
129 .map(|auth| {
130 HttpAuthentication::deserialize(auth)
131 .map_err(D::Error::custom)
132 })
133 .transpose()?,
134 },
135 }
136 }
137 "ssh" => Location::GitOverSsh {
138 transport: SshTransport::deserialize(&root).map_err(D::Error::custom)?,
139 git_options: GitOptions::deserialize(&root).map_err(D::Error::custom)?,
140 authentication: root
141 .at(AUTHENTICATION_KEY)
142 .map(|auth| {
143 SshAuthentication::deserialize(auth).map_err(D::Error::custom)
144 })
145 .transpose()?,
146 },
147 "git" => Location::Git {
148 options: GitOptions::deserialize(&root).map_err(D::Error::custom)?,
149 },
150 "npm" => Location::Npm {
151 options: NpmOptions::deserialize(&root).map_err(D::Error::custom)?,
152 },
153 "cargo" => Location::Cargo {
154 options: CargoOptions::deserialize(&root).map_err(D::Error::custom)?,
155 },
156 invalid_scheme => {
157 return Err(serde::de::Error::custom(&format!(
158 "invalid url scheme \"{invalid_scheme}\""
159 )))
160 }
161 },
162 url,
163 }),
164 }
165 }
166}
167
168#[derive(Hash, Debug, PartialEq, Eq, Clone)]
169pub enum Location {
170 LocalFileSystem {
171 options: FileSystemOptions,
172 },
173
174 TarballOverHttp {
175 transport: HttpTransport,
176 tarball_options: TarballOptions,
177 authentication: Option<HttpAuthentication>,
178 },
179
180 GitOverHttp {
181 transport: HttpTransport,
182 git_options: GitOptions,
183 authentication: Option<GitPlainAuthentication>,
184 },
185
186 GitOverSsh {
187 transport: SshTransport,
188 git_options: GitOptions,
189 authentication: Option<SshAuthentication>,
190 },
191 Git {
192 options: GitOptions,
193 },
194 Cargo {
195 options: CargoOptions,
196 },
197 Npm {
198 options: NpmOptions,
199 },
200}
201
202impl Serialize for Location {
203 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
204 where
205 S: serde::Serializer,
206 {
207 match self {
208 Self::LocalFileSystem { options } => options.serialize(serializer),
209 Self::GitOverHttp {
210 transport,
211 git_options,
212 authentication,
213 } => {
214 let mut value = to_value(transport).map_err(S::Error::custom)?;
215 value.overwrite(to_value(git_options).map_err(S::Error::custom)?);
216 value.overwrite(Value::object([(
217 FORMAT_KEY,
218 Value::string(HttpFormatIdentifier::Git.to_string()),
219 )]));
220 if let Some(authentication) = authentication {
221 value.overwrite(Value::object([(
222 AUTHENTICATION_KEY,
223 to_value(authentication).map_err(S::Error::custom)?,
224 )]));
225 }
226 value.serialize(serializer)
227 }
228 Self::GitOverSsh {
229 transport,
230 git_options,
231 authentication,
232 } => {
233 let mut value = to_value(transport).map_err(S::Error::custom)?;
234 value.overwrite(to_value(git_options).map_err(S::Error::custom)?);
235 if let Some(authentication) = authentication {
236 value.overwrite(to_value(authentication).map_err(S::Error::custom)?);
237 }
238 value.serialize(serializer)
239 }
240 Self::Npm { options } => options.serialize(serializer),
241 Self::Cargo { options } => options.serialize(serializer),
242 _ => todo!(),
243 }
244 }
245}
246
247#[derive(Default, EnumIter, Display, Serialize, Hash, PartialEq, Eq, Debug, Clone, Copy)]
248pub enum RebuildStrategy {
249 Always,
250 #[default]
251 OnChanges,
252}
253
254unit_enum_from_str!(RebuildStrategy);
255unit_enum_deserialize!(RebuildStrategy);
256
257#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone, Default)]
258pub struct FileSystemOptions {
259 #[serde(skip_serializing_if = "Option::is_none")]
260 kind: Option<ExecutorKind>,
261 #[serde(default)]
262 rebuild: RebuildStrategy,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 watch: Option<BTreeSet<FileChangesMatcher>>,
265}
266
267impl FileSystemOptions {
268 pub fn kind(&self) -> Option<ExecutorKind> {
269 self.kind
270 }
271
272 pub fn rebuild(&self) -> RebuildStrategy {
273 self.rebuild
274 }
275
276 pub fn watch(&self) -> Option<&BTreeSet<FileChangesMatcher>> {
277 self.watch.as_ref()
278 }
279}
280
281#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
282pub struct HttpTransport {
283 #[serde(default)]
284 insecure: bool,
285 #[serde(default)]
286 headers: BTreeMap<String, String>,
287}
288
289impl HttpTransport {
290 pub fn insecure(&self) -> bool {
291 self.insecure
292 }
293
294 pub fn headers(&self) -> &BTreeMap<String, String> {
295 &self.headers
296 }
297}
298
299const HTTP_AUTH_MODE_KEY: &str = "mode";
300
301#[derive(Serialize, EnumIter, Display, Hash, PartialEq, Eq, Clone)]
302pub enum HttpAuthenticationMode {
303 Basic,
304 Bearer,
305}
306
307unit_enum_from_str!(HttpAuthenticationMode);
308unit_enum_deserialize!(HttpAuthenticationMode);
309
310#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
311pub struct HttpBasicAuthentication {
312 username: String,
313 password: String,
314}
315
316impl HttpBasicAuthentication {
317 pub fn username(&self) -> &str {
318 &self.username
319 }
320
321 pub fn password(&self) -> &str {
322 &self.password
323 }
324}
325
326#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
327pub struct HttpDigestAuthentication {
328 username: String,
329 password: String,
330}
331
332impl HttpDigestAuthentication {
333 pub fn username(&self) -> &str {
334 &self.username
335 }
336
337 pub fn password(&self) -> &str {
338 &self.password
339 }
340}
341
342#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
343pub struct HttpBearerAuthentication {
344 token: String,
345}
346
347impl HttpBearerAuthentication {
348 pub fn token(&self) -> &str {
349 &self.token
350 }
351}
352
353#[derive(Hash, Debug, PartialEq, Eq, Clone)]
354pub enum HttpAuthentication {
355 Basic(HttpBasicAuthentication),
356 Bearer(HttpBearerAuthentication),
357}
358
359impl<'de> Deserialize<'de> for HttpAuthentication {
360 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
361 where
362 D: serde::Deserializer<'de>,
363 {
364 let root = Value::deserialize(deserializer)?;
365 let mode = root
366 .at(HTTP_AUTH_MODE_KEY)
367 .map(|m| HttpAuthenticationMode::deserialize(m).map_err(D::Error::custom))
368 .transpose()?
369 .ok_or_else(|| D::Error::missing_field(HTTP_AUTH_MODE_KEY))?;
370 Ok(match mode {
371 HttpAuthenticationMode::Basic => HttpAuthentication::Basic(
372 HttpBasicAuthentication::deserialize(root).map_err(D::Error::custom)?,
373 ),
374 HttpAuthenticationMode::Bearer => HttpAuthentication::Bearer(
375 HttpBearerAuthentication::deserialize(root).map_err(D::Error::custom)?,
376 ),
377 })
378 }
379}
380
381impl Serialize for HttpAuthentication {
382 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
383 where
384 S: serde::Serializer,
385 {
386 let mut value = Value::object([(
387 HTTP_AUTH_MODE_KEY,
388 to_value(match self {
389 HttpAuthentication::Basic(_) => HttpAuthenticationMode::Basic,
390 HttpAuthentication::Bearer(_) => HttpAuthenticationMode::Bearer,
391 })
392 .map_err(S::Error::custom)?,
393 )]);
394
395 value.overwrite(
396 match self {
397 HttpAuthentication::Basic(basic) => to_value(basic),
398 HttpAuthentication::Bearer(bearer) => to_value(bearer),
399 }
400 .map_err(S::Error::custom)?,
401 );
402
403 value.serialize(serializer)
404 }
405}
406
407#[derive(Serialize, EnumIter, Display)]
408pub enum HttpFormatIdentifier {
409 Git,
410 Tarball,
411}
412
413#[derive(Serialize, Hash, Debug, PartialEq, Eq, Clone)]
414#[serde(untagged)]
415pub enum HttpResource {
416 Git {
417 #[serde(flatten)]
418 options: GitOptions,
419 },
420 Tarball {
421 #[serde(flatten)]
422 options: TarballOptions,
423 },
424}
425
426#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
427#[serde(untagged)]
428pub enum GitCheckout {
429 Branch { branch: String },
430 Tag { tag: String },
431 Revision { rev: String },
432}
433
434impl Display for GitCheckout {
435 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436 f.write_str(match self {
437 Self::Branch { branch } => branch.as_str(),
438 Self::Revision { rev } => rev.as_str(),
439 Self::Tag { tag } => tag.as_str(),
440 })
441 }
442}
443
444unit_enum_from_str!(HttpFormatIdentifier);
445unit_enum_deserialize!(HttpFormatIdentifier);
446
447#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
448pub struct GitOptions {
449 #[serde(skip_serializing_if = "Option::is_none")]
450 kind: Option<ExecutorKind>,
451 #[serde(default)]
452 pull: bool,
453 #[serde(skip_serializing_if = "Option::is_none", flatten)]
454 checkout: Option<GitCheckout>,
455 #[serde(skip_serializing_if = "Option::is_none")]
456 path: Option<PathBuf>,
457}
458
459impl GitOptions {
460 pub fn kind(&self) -> Option<ExecutorKind> {
461 self.kind
462 }
463
464 pub fn pull(&self) -> bool {
465 self.pull
466 }
467
468 pub fn checkout(&self) -> Option<&GitCheckout> {
469 self.checkout.as_ref()
470 }
471
472 pub fn path(&self) -> Option<&Path> {
473 self.path.as_deref()
474 }
475}
476
477#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
478pub struct GitPlainAuthentication {
479 username: String,
480 password: String,
481}
482
483impl GitPlainAuthentication {
484 pub fn username(&self) -> &str {
485 &self.username
486 }
487
488 pub fn password(&self) -> &str {
489 &self.password
490 }
491}
492
493#[derive(Serialize, EnumIter, Display, Hash, Debug, PartialEq, Eq, Copy, Clone)]
494pub enum Compression {
495 Deflate,
496 Zlib,
497 Gzip,
498}
499
500unit_enum_from_str!(Compression);
501unit_enum_deserialize!(Compression);
502
503#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, Clone)]
504pub struct TarballOptions {
505 #[serde(skip_serializing_if = "Option::is_none")]
506 kind: Option<ExecutorKind>,
507 #[serde(skip_serializing_if = "Option::is_none")]
508 compression: Option<Compression>,
509}
510
511impl TarballOptions {
512 pub fn kind(&self) -> Option<ExecutorKind> {
513 self.kind
514 }
515
516 pub fn compression(&self) -> Option<Compression> {
517 self.compression
518 }
519}
520
521#[derive(Serialize, EnumIter, Display, Hash, Debug, PartialEq, Eq, Copy, Clone)]
522pub enum SshFingerprintAlgorithm {
523 Md5,
524 Sha1,
525 Sha256,
526}
527
528unit_enum_from_str!(SshFingerprintAlgorithm);
529unit_enum_deserialize!(SshFingerprintAlgorithm);
530
531#[derive(Hash, Debug, PartialEq, Eq, Clone)]
532pub enum SshFingerprint {
533 Md5([u8; 16]),
534 Sha1([u8; 20]),
535 Sha256([u8; 32]),
536}
537
538macro_rules! parse_fingerprint {
539 ($value:expr, $len:literal) => {{
540 let decoded = base64::prelude::BASE64_STANDARD_NO_PAD
541 .decode($value)
542 .map_err(|err| {
543 anyhow::anyhow!(
544 "failed to decode ssh fingerprint content {} ({})",
545 $value,
546 err
547 )
548 })?;
549 let length = decoded.len();
550 if length != $len {
551 bail!(
552 "fingerprint has invalid length (expected={}, actual={})",
553 $len,
554 length
555 )
556 }
557 let mut bytes = [0_u8; $len];
558 bytes.copy_from_slice(&decoded);
559 Ok::<_, $crate::error::Error>(bytes)
560 }};
561}
562
563const MD5: &str = "MD5";
564const SHA1: &str = "SHA1";
565const SHA256: &str = "SHA256";
566
567impl FromStr for SshFingerprint {
568 type Err = Error;
569
570 fn from_str(s: &str) -> crate::error::Result<Self> {
571 Ok(match s.split_once(':'){
572 Some((algorithm, value)) => {
573 match algorithm {
574 MD5 => Self::Md5(parse_fingerprint!(value, 16)?),
575 SHA1 => Self::Sha1(parse_fingerprint!(value, 20)?),
576 SHA256 => Self::Sha256(parse_fingerprint!(value, 32)?),
577 _ => bail!("unknown ssh fingerprint algorithm \"{algorithm}\". valid algorithms are MD5, SHA1 and SHA256")
578 }
579 },
580 _ => bail!("bad ssh fingerprint. format must be <ALGORITHM>:<base64 hash>.")
581 })
582 }
583}
584
585impl Display for SshFingerprint {
586 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
587 let (algorithm_id, slice) = match self {
588 Self::Md5(md5) => (MD5, md5.as_slice()),
589 Self::Sha1(sha1) => (SHA1, sha1.as_slice()),
590 Self::Sha256(sha256) => (SHA256, sha256.as_slice()),
591 };
592 let mut base64 = String::new();
593 base64::prelude::BASE64_STANDARD_NO_PAD.encode_string(slice, &mut base64);
594 write!(f, "{algorithm_id}:{base64}")
595 }
596}
597
598impl Serialize for SshFingerprint {
599 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
600 where
601 S: serde::Serializer,
602 {
603 serializer.serialize_str(&self.to_string())
604 }
605}
606
607impl<'de> Deserialize<'de> for SshFingerprint {
608 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
609 where
610 D: serde::Deserializer<'de>,
611 {
612 let s = String::deserialize(deserializer)?;
613 SshFingerprint::from_str(&s).map_err(D::Error::custom)
614 }
615}
616
617#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
618#[serde(rename_all = "camelCase")]
619pub struct SshTransport {
620 #[serde(skip_serializing_if = "Option::is_none")]
621 fingerprints: Option<Vec<SshFingerprint>>,
622 #[serde(default)]
623 insecure: bool,
624}
625
626impl SshTransport {
627 pub fn fingerprints(&self) -> Option<&[SshFingerprint]> {
628 self.fingerprints.as_deref()
629 }
630
631 pub fn insecure(&self) -> bool {
632 self.insecure
633 }
634}
635
636#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
637#[serde(untagged)]
638pub enum SshResource {
639 Git {
640 #[serde(flatten)]
641 options: GitOptions,
642 },
643}
644
645#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
646#[serde(untagged)]
647pub enum SshAuthentication {
648 Password {
649 #[serde(skip_serializing_if = "Option::is_none")]
650 username: Option<String>,
651 password: String,
652 },
653 PrivateKeyFile {
654 #[serde(skip_serializing_if = "Option::is_none")]
655 username: Option<String>,
656 key: PathBuf,
657 #[serde(skip_serializing_if = "Option::is_none")]
658 passphrase: Option<String>,
659 },
660}
661
662#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
663pub struct CargoOptions {
664 #[serde(skip_serializing_if = "Option::is_none")]
665 version: Option<String>,
666 #[serde(default)]
667 insecure: bool,
668 #[serde(skip_serializing_if = "Option::is_none")]
669 token: Option<String>,
670}
671
672impl CargoOptions {
673 pub fn version(&self) -> Option<&str> {
674 self.version.as_deref()
675 }
676
677 pub fn insecure(&self) -> bool {
678 self.insecure
679 }
680
681 pub fn token(&self) -> Option<&str> {
682 self.token.as_deref()
683 }
684}
685
686#[derive(Deserialize, Serialize, Hash, Debug, PartialEq, Eq, Clone)]
687pub struct NpmOptions {
688 #[serde(skip_serializing_if = "Option::is_none")]
689 version: Option<String>,
690 #[serde(default)]
691 insecure: bool,
692 #[serde(skip_serializing_if = "Option::is_none")]
693 token: Option<String>,
694}
695
696impl NpmOptions {
697 pub fn version(&self) -> Option<&str> {
698 self.version.as_deref()
699 }
700
701 pub fn insecure(&self) -> bool {
702 self.insecure
703 }
704
705 pub fn token(&self) -> Option<&str> {
706 self.token.as_deref()
707 }
708}