///|
pub struct UpdateArtifactTrust {
path : String
sha256 : String
signature : String?
} derive(Debug, Eq)
///|
pub struct UpdateManifestArtifact {
target : BundleTarget
kind : BundleArtifactKind
path : String
url : String
sha256 : String
signature : String?
} derive(Debug, Eq)
///|
pub struct UpdateManifest {
identifier : String
product_name : String
version : String
channel : String
public_key : String?
artifacts : Array[UpdateManifestArtifact]
} derive(Debug, Eq)
///|
pub struct UpdateTrustReport {
trusted : Bool
signature_required : Bool
artifact_count : Int
signed_artifacts : Int
problems : Array[String]
} derive(Debug, Eq)
///|
pub fn UpdateArtifactTrust::new(
path~ : String,
sha256~ : String,
signature? : String,
) -> UpdateArtifactTrust {
{ path, sha256, signature }
}
///|
pub fn UpdateArtifactTrust::path(self : UpdateArtifactTrust) -> String {
self.path
}
///|
pub fn UpdateArtifactTrust::sha256(self : UpdateArtifactTrust) -> String {
self.sha256
}
///|
pub fn UpdateArtifactTrust::signature(self : UpdateArtifactTrust) -> String? {
self.signature
}
///|
pub fn UpdateArtifactTrust::validate(
self : UpdateArtifactTrust,
) -> Array[String] {
let problems : Array[String] = []
if self.path == "" {
problems.push("update artifact trust path is required")
}
if !update_sha256_valid(self.sha256) {
problems.push(
"update artifact sha256 must be 64 lowercase hex characters: \{self.path}",
)
}
if self.signature is Some("") {
problems.push("update artifact signature must not be empty: \{self.path}")
}
problems
}
///|
pub fn UpdateManifestArtifact::new(
target~ : BundleTarget,
kind~ : BundleArtifactKind,
path~ : String,
url~ : String,
sha256~ : String,
signature? : String,
) -> UpdateManifestArtifact {
{ target, kind, path, url, sha256, signature }
}
///|
pub fn UpdateManifestArtifact::target(
self : UpdateManifestArtifact,
) -> BundleTarget {
self.target
}
///|
pub fn UpdateManifestArtifact::kind(
self : UpdateManifestArtifact,
) -> BundleArtifactKind {
self.kind
}
///|
pub fn UpdateManifestArtifact::path(self : UpdateManifestArtifact) -> String {
self.path
}
///|
pub fn UpdateManifestArtifact::url(self : UpdateManifestArtifact) -> String {
self.url
}
///|
pub fn UpdateManifestArtifact::sha256(self : UpdateManifestArtifact) -> String {
self.sha256
}
///|
pub fn UpdateManifestArtifact::signature(
self : UpdateManifestArtifact,
) -> String? {
self.signature
}
///|
pub fn UpdateManifestArtifact::validate(
self : UpdateManifestArtifact,
signature_required? : Bool = false,
) -> Array[String] {
let problems : Array[String] = []
if self.path == "" {
problems.push("update artifact path is required")
}
if self.url == "" {
problems.push("update artifact url is required: \{self.path}")
} else if !self.url.has_prefix("https://") {
problems.push("update artifact url must use https: \{self.path}")
}
if !update_sha256_valid(self.sha256) {
problems.push(
"update artifact sha256 must be 64 lowercase hex characters: \{self.path}",
)
}
match self.signature {
Some("") =>
problems.push("update artifact signature must not be empty: \{self.path}")
Some(_) => ()
None =>
if signature_required {
problems.push("update artifact signature is required: \{self.path}")
}
}
problems
}
///|
pub fn UpdateManifestArtifact::to_json(self : UpdateManifestArtifact) -> String {
[
"{",
"\"target\":\{self.target.name().json_string()},",
"\"kind\":\{self.kind.name().json_string()},",
"\"path\":\{self.path.json_string()},",
"\"url\":\{self.url.json_string()},",
"\"sha256\":\{self.sha256.json_string()},",
"\"signature\":\{optional_update_json_string(self.signature)}",
"}",
].join("")
}
///|
pub fn UpdateManifest::new(
identifier~ : String,
product_name~ : String,
version~ : String,
channel? : String = "stable",
public_key? : String,
artifacts~ : Array[UpdateManifestArtifact],
) -> UpdateManifest {
{ identifier, product_name, version, channel, public_key, artifacts }
}
///|
pub fn UpdateManifest::identifier(self : UpdateManifest) -> String {
self.identifier
}
///|
pub fn UpdateManifest::product_name(self : UpdateManifest) -> String {
self.product_name
}
///|
pub fn UpdateManifest::version(self : UpdateManifest) -> String {
self.version
}
///|
pub fn UpdateManifest::channel(self : UpdateManifest) -> String {
self.channel
}
///|
pub fn UpdateManifest::public_key(self : UpdateManifest) -> String? {
self.public_key
}
///|
pub fn UpdateManifest::artifacts(
self : UpdateManifest,
) -> Array[UpdateManifestArtifact] {
self.artifacts.copy()
}
///|
pub fn UpdateManifest::validate(self : UpdateManifest) -> Array[String] {
let problems : Array[String] = []
if self.identifier == "" {
problems.push("update identifier is required")
}
if self.product_name == "" {
problems.push("update product name is required")
}
if self.version == "" {
problems.push("update version is required")
}
if !update_channel_valid(self.channel) {
problems.push("update channel is invalid: \{self.channel}")
}
if self.public_key is Some("") {
problems.push("update public key must not be empty")
}
if self.artifacts.is_empty() {
problems.push("update artifacts must not be empty")
}
let signature_required = self.public_key is Some(_)
for artifact in self.artifacts {
for problem in artifact.validate(signature_required~) {
problems.push(problem)
}
}
problems
}
///|
pub fn UpdateManifest::trust_report(self : UpdateManifest) -> UpdateTrustReport {
let problems = self.validate()
let signature_required = self.public_key is Some(_)
let mut signed_artifacts = 0
for artifact in self.artifacts {
if artifact.signature() is Some(_) {
signed_artifacts += 1
}
}
if !signature_required {
problems.push("update public key is required for trusted native updates")
}
{
trusted: problems.is_empty(),
signature_required,
artifact_count: self.artifacts.length(),
signed_artifacts,
problems,
}
}
///|
pub fn UpdateManifest::to_json(self : UpdateManifest) -> String {
[
"{",
"\"schemaVersion\":1,",
"\"identifier\":\{self.identifier.json_string()},",
"\"productName\":\{self.product_name.json_string()},",
"\"version\":\{self.version.json_string()},",
"\"channel\":\{self.channel.json_string()},",
"\"publicKey\":\{optional_update_json_string(self.public_key)},",
"\"artifacts\":[\{self.artifacts.map(fn(artifact) { artifact.to_json() }).join(",")}]",
"}",
].join("")
}
///|
pub fn UpdateTrustReport::trusted(self : UpdateTrustReport) -> Bool {
self.trusted
}
///|
pub fn UpdateTrustReport::signature_required(self : UpdateTrustReport) -> Bool {
self.signature_required
}
///|
pub fn UpdateTrustReport::artifact_count(self : UpdateTrustReport) -> Int {
self.artifact_count
}
///|
pub fn UpdateTrustReport::signed_artifacts(self : UpdateTrustReport) -> Int {
self.signed_artifacts
}
///|
pub fn UpdateTrustReport::problems(self : UpdateTrustReport) -> Array[String] {
self.problems.copy()
}
///|
pub fn UpdateTrustReport::to_json(self : UpdateTrustReport) -> String {
[
"{",
"\"trusted\":\{self.trusted.json_bool()},",
"\"signatureRequired\":\{self.signature_required.json_bool()},",
"\"artifactCount\":\{self.artifact_count},",
"\"signedArtifacts\":\{self.signed_artifacts},",
"\"problems\":[\{self.problems.map(fn(problem) { problem.json_string() }).join(",")}]",
"}",
].join("")
}
///|
pub fn BundlePlan::update_manifest(
self : BundlePlan,
base_url~ : String,
channel? : String = "stable",
public_key? : String,
artifact_trust? : Array[UpdateArtifactTrust] = [],
) -> Result[UpdateManifest, Array[String]] {
let problems : Array[String] = []
if base_url == "" {
problems.push("update base url is required")
} else if !base_url.has_prefix("https://") {
problems.push("update base url must use https")
}
let artifact_paths = self.artifacts().map(fn(artifact) { artifact.path() })
let trust_paths : Array[String] = []
for trust in artifact_trust {
for problem in trust.validate() {
problems.push(problem)
}
if trust.path() != "" && trust_paths.contains(trust.path()) {
problems.push(
"update artifact trust path must be unique: \{trust.path()}",
)
} else {
trust_paths.push(trust.path())
}
if trust.path() != "" && !artifact_paths.contains(trust.path()) {
problems.push(
"update artifact trust path is not a bundle artifact: \{trust.path()}",
)
}
}
let update_artifacts : Array[UpdateManifestArtifact] = []
for artifact in self.artifacts() {
match update_trust_for_path(artifact_trust, artifact.path()) {
Some(trust) =>
update_artifacts.push(
UpdateManifestArtifact::new(
target=self.target(),
kind=artifact.kind(),
path=artifact.path(),
url=update_artifact_url(base_url, artifact.path()),
sha256=trust.sha256(),
signature?=trust.signature(),
),
)
None =>
problems.push("update artifact trust is missing: \{artifact.path()}")
}
}
let manifest = UpdateManifest::new(
identifier=self.identifier(),
product_name=self.metadata.product_name(),
version=self.metadata.version(),
channel~,
public_key?,
artifacts=update_artifacts,
)
for problem in manifest.validate() {
problems.push(problem)
}
if problems.is_empty() {
Ok(manifest)
} else {
Err(problems)
}
}
///|
pub fn BundlePlan::update_manifest_json(
self : BundlePlan,
base_url~ : String,
channel? : String = "stable",
public_key? : String,
artifact_trust? : Array[UpdateArtifactTrust] = [],
) -> Result[String, Array[String]] {
match
self.update_manifest(base_url~, channel~, public_key?, artifact_trust~) {
Ok(manifest) => Ok(manifest.to_json())
Err(problems) => Err(problems)
}
}
///|
fn update_trust_for_path(
trusts : Array[UpdateArtifactTrust],
path : String,
) -> UpdateArtifactTrust? {
for trust in trusts {
if trust.path() == path {
return Some(trust)
}
}
None
}
///|
fn update_artifact_url(base_url : String, path : String) -> String {
if base_url.has_suffix("/") {
base_url + path
} else {
base_url + "/" + path
}
}
///|
fn update_sha256_valid(value : String) -> Bool {
value.length() == 64 &&
value
.to_lower()
.replace_all(old="0", new="")
.replace_all(old="1", new="")
.replace_all(old="2", new="")
.replace_all(old="3", new="")
.replace_all(old="4", new="")
.replace_all(old="5", new="")
.replace_all(old="6", new="")
.replace_all(old="7", new="")
.replace_all(old="8", new="")
.replace_all(old="9", new="")
.replace_all(old="a", new="")
.replace_all(old="b", new="")
.replace_all(old="c", new="")
.replace_all(old="d", new="")
.replace_all(old="e", new="")
.replace_all(old="f", new="")
.is_empty()
}
///|
fn update_channel_valid(channel : String) -> Bool {
channel == "stable" ||
channel == "beta" ||
channel == "nightly" ||
channel == "dev"
}
///|
fn optional_update_json_string(value : String?) -> String {
match value {
Some(text) => text.json_string()
None => "null"
}
}