1use serde::{Deserialize, Serialize};
2use opys_mojang_rules::{satisfies_ruleset, OsOptions, RuleError, Ruleset};
3
4use crate::discovery::Discovery;
5use crate::extract::{decode_extract, encode_extract, ExtractRule, ExtractWire};
6use crate::integrity::Integrity;
7use crate::shorthand::{encode_short_ruleset, parse_short_ruleset, RawRuleset, ShorthandError};
8use crate::source::{decode_source, encode_source, Source, SourceWire};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Artifact {
12 pub path: String,
13 pub source: Source,
14 pub size: Option<u64>,
15 pub rules: Ruleset,
16 pub integrity: Option<Integrity>,
17 pub discovery: Option<Discovery>,
18 pub metadata: Option<serde_json::Value>,
19 pub extract: Option<Vec<ExtractRule>>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ArtifactWire {
24 pub path: String,
25 pub source: SourceWire,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub size: Option<u64>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub rules: Option<RawRuleset>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub integrity: Option<Integrity>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub discovery: Option<Discovery>,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub metadata: Option<serde_json::Value>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub extract: Option<ExtractWire>,
38}
39
40pub fn decode_artifact(raw: ArtifactWire) -> Result<Artifact, ShorthandError> {
41 Ok(Artifact {
42 path: raw.path,
43 source: decode_source(raw.source),
44 size: raw.size,
45 rules: raw.rules.map(parse_short_ruleset).transpose()?.unwrap_or_default(),
46 integrity: raw.integrity,
47 discovery: raw.discovery,
48 metadata: raw.metadata,
49 extract: raw.extract.map(decode_extract),
50 })
51}
52
53pub fn encode_artifact(u: &Artifact) -> ArtifactWire {
54 ArtifactWire {
55 path: u.path.clone(),
56 source: encode_source(&u.source),
57 size: u.size,
58 rules: (!u.rules.is_empty()).then(|| encode_short_ruleset(&u.rules)),
59 integrity: u.integrity.clone().map(Integrity::collapsed),
60 discovery: u.discovery.clone(),
61 metadata: u.metadata.clone(),
62 extract: u.extract.as_deref().map(encode_extract),
63 }
64}
65
66pub fn deduplicate_artifacts(artifacts: Vec<Artifact>) -> Vec<Artifact> {
68 use indexmap::IndexMap;
69 let mut map: IndexMap<String, Artifact> = IndexMap::new();
70 for u in artifacts {
71 let norm = normalize_posix(&u.path);
72 map.shift_remove(&norm);
73 map.insert(norm, u);
74 }
75 map.into_values().collect()
76}
77
78fn normalize_posix(p: &str) -> String {
79 let mut stack: Vec<&str> = Vec::new();
81 let leading_slash = p.starts_with('/');
82 for part in p.split('/') {
83 match part {
84 "" | "." => continue,
85 ".." => {
86 if matches!(stack.last(), Some(&prev) if prev != "..") {
87 stack.pop();
88 } else if !leading_slash {
89 stack.push("..");
90 }
91 }
92 other => stack.push(other),
93 }
94 }
95 let joined = stack.join("/");
96 if leading_slash {
97 format!("/{joined}")
98 } else if joined.is_empty() {
99 ".".to_owned()
100 } else {
101 joined
102 }
103}
104
105impl Artifact {
106 pub fn applies(&self, os: &OsOptions, feats: &[String]) -> Result<bool, RuleError> {
108 satisfies_ruleset(&self.rules, os, feats)
109 }
110}
111
112pub fn artifact_applies(
114 u: &Artifact,
115 os: &OsOptions,
116 feats: &[String],
117) -> Result<bool, RuleError> {
118 u.applies(os, feats)
119}