Skip to main content

opys_core/
artifact.rs

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
66/// Deduplicate by normalized path — later entries win.
67pub 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    // Approximate POSIX `path.normalize` — collapse `./`, `//`, resolve `..`.
80    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    /// True if the artifact's ruleset matches the given platform + features.
107    pub fn applies(&self, os: &OsOptions, feats: &[String]) -> Result<bool, RuleError> {
108        satisfies_ruleset(&self.rules, os, feats)
109    }
110}
111
112/// Free-fn alias for `Artifact::applies` — kept until callers migrate.
113pub fn artifact_applies(
114    u: &Artifact,
115    os: &OsOptions,
116    feats: &[String],
117) -> Result<bool, RuleError> {
118    u.applies(os, feats)
119}