Files
nap/src/repo/mod.rs

850 lines
26 KiB
Rust

use crate::manifest::Manifest;
use crate::repo::github::GithubRepo;
use anyhow::{anyhow, bail, ensure, Result};
use apk::res::Chunk;
use apk::zip::ZipArchive;
use apk::AndroidManifest;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use log::{debug, info, warn};
use nostr_sdk::prelude::{hex, Coordinate, StreamExt};
use nostr_sdk::{Event, EventBuilder, Kind, NostrSigner, Tag};
use reqwest::Url;
use semver::Version;
use sha2::{Digest, Sha256};
use std::collections::{HashMap, HashSet};
use std::env::temp_dir;
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use tokio::io::AsyncWriteExt;
mod github;
/// Since artifact binary / image
#[derive(Debug, Clone)]
pub struct RepoArtifact {
/// Artifact name (filename)
pub name: String,
/// The size of the artifact in bytes
pub size: u64,
/// Where the artifact is located
pub location: RepoResource,
/// MIME type
pub content_type: String,
/// Platform this artifact runs on
pub platform: Platform,
/// Artifact metadata
pub metadata: ArtifactMetadata,
/// SHA-256 hash of the artifact
pub hash: Option<Vec<u8>>,
}
impl Display for RepoArtifact {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} platform={} metadata={}",
self.name, self.platform, self.metadata
)
}
}
/// Converts a repo artifact into a NIP-94 event
impl TryInto<EventBuilder> for RepoArtifact {
type Error = anyhow::Error;
fn try_into(self) -> Result<EventBuilder, Self::Error> {
let mut b = EventBuilder::new(Kind::FileMetadata, "").tags([
Tag::parse(["f", self.platform.to_string().as_str()])?,
Tag::parse(["m", self.content_type.as_str()])?,
Tag::parse(["size", self.size.to_string().as_str()])?,
]);
if let RepoResource::Remote(u) = self.location {
b = b.tag(Tag::parse(["url", u.as_str()])?);
}
match self.metadata {
ArtifactMetadata::APK {
manifest,
signature,
} => {
match signature {
ApkSignatureBlock::None => {
warn!("No signature found in metadata");
}
ApkSignatureBlock::V2 { signatures, .. } => {
for signature in signatures {
b = b.tag(Tag::parse([
"apk_signature_hash",
&hex::encode(signature.digest),
])?);
}
}
ApkSignatureBlock::V3 { signatures, .. } => {
for signature in signatures {
b = b.tag(Tag::parse([
"apk_signature_hash",
&hex::encode(signature.digest),
])?);
}
}
}
if let Some(vn) = manifest.version_name {
b = b.tag(Tag::parse(["version", vn.as_str()])?);
}
if let Some(vc) = manifest.version_code {
b = b.tag(Tag::parse(["version_code", vc.to_string().as_str()])?);
}
if let Some(min_sdk) = manifest.sdk.min_sdk_version {
b = b.tag(Tag::parse([
"min_sdk_version",
min_sdk.to_string().as_str(),
])?);
}
if let Some(target_sdk) = manifest.sdk.target_sdk_version {
b = b.tag(Tag::parse([
"target_sdk_version",
target_sdk.to_string().as_str(),
])?);
}
//TODO: apk sig
}
}
Ok(b)
}
}
#[derive(Debug, Clone)]
pub enum ArtifactMetadata {
APK {
manifest: AndroidManifest,
signature: ApkSignatureBlock,
},
}
#[derive(Debug, Clone)]
pub enum ApkSignatureBlock {
None,
/// Android V2 Signature Block
///
/// https://source.android.com/docs/security/features/apksigning/v2#apk-signature-scheme-v2-block-format
V2 {
signatures: Vec<ApkSignature>,
public_key: Vec<u8>,
certificates: Vec<Vec<u8>>,
attributes: HashMap<u32, Vec<u8>>,
},
V3 {
signatures: Vec<ApkSignature>,
public_key: Vec<u8>,
},
}
impl Display for ApkSignatureBlock {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApkSignatureBlock::None => write!(f, "none"),
ApkSignatureBlock::V2 { signatures, .. } => {
write!(f, "v2: ")?;
for sig in signatures {
write!(
f,
"algo={}, digest={}, sig={}",
sig.algo,
hex::encode(&sig.digest),
hex::encode(&sig.signature)
)?;
}
Ok(())
}
ApkSignatureBlock::V3 { signatures, .. } => {
write!(f, "V3: ")?;
for sig in signatures {
write!(
f,
"algo={}, digest={}, sig={}",
sig.algo,
hex::encode(&sig.digest),
hex::encode(&sig.signature)
)?;
}
Ok(())
}
}
}
}
#[derive(Debug, Clone)]
pub struct ApkSignature {
pub algo: ApkSignatureAlgo,
pub signature: Vec<u8>,
pub digest: Vec<u8>,
}
#[derive(Debug, Clone)]
pub enum ApkSignatureAlgo {
RsaSsaPssSha256,
RsaSsaPssSha512,
RsaSsaPkcs1Sha256,
RsaSsaPkcs1Sha512,
EcdsaSha256,
EcdsaSha512,
DsaSha256,
}
impl Display for ApkSignatureAlgo {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApkSignatureAlgo::RsaSsaPssSha256 => write!(f, "RSASSA-PSS-SHA256"),
ApkSignatureAlgo::RsaSsaPssSha512 => write!(f, "RSASSA-PSS-SHA512"),
ApkSignatureAlgo::RsaSsaPkcs1Sha256 => write!(f, "RSASSA-PKCS1-SHA256"),
ApkSignatureAlgo::RsaSsaPkcs1Sha512 => write!(f, "RSASSA-PKCS1-SHA512"),
ApkSignatureAlgo::EcdsaSha256 => write!(f, "ECDSA-SHA256"),
ApkSignatureAlgo::EcdsaSha512 => write!(f, "ECDSA-SHA512"),
ApkSignatureAlgo::DsaSha256 => write!(f, "DSA-SHA256"),
}
}
}
impl TryFrom<u32> for ApkSignatureAlgo {
type Error = anyhow::Error;
fn try_from(value: u32) -> std::result::Result<Self, Self::Error> {
match value {
0x0101 => Ok(ApkSignatureAlgo::RsaSsaPssSha256),
0x0102 => Ok(ApkSignatureAlgo::RsaSsaPssSha512),
0x0103 => Ok(ApkSignatureAlgo::RsaSsaPkcs1Sha256),
0x0104 => Ok(ApkSignatureAlgo::RsaSsaPkcs1Sha512),
0x0201 => Ok(ApkSignatureAlgo::EcdsaSha256),
0x0202 => Ok(ApkSignatureAlgo::EcdsaSha512),
0x0301 => Ok(ApkSignatureAlgo::DsaSha256),
_ => bail!("Unknown signature algo"),
}
}
}
impl Display for ArtifactMetadata {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ArtifactMetadata::APK {
manifest,
signature,
} => {
write!(
f,
"APK id={}, version={}, code={}, sig={}",
manifest.package.as_ref().unwrap_or(&"missing".to_string()),
manifest.version_name.as_ref().unwrap_or(&String::new()),
manifest.version_code.as_ref().unwrap_or(&0),
signature
)
}
}
}
}
#[derive(Debug, Clone)]
pub enum Platform {
Android { arch: Architecture },
IOS { arch: Architecture },
MacOS { arch: Architecture },
Windows { arch: Architecture },
Linux { arch: Architecture },
Web,
}
impl Display for Platform {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Platform::Android { arch } => write!(
f,
"android-{}",
match arch {
Architecture::ARMv7 => "armeabi-v7a",
Architecture::ARM64 => "arm64-v8a",
Architecture::X86 => "x86",
Architecture::X86_64 => "x86_64",
}
),
Platform::IOS { arch } => write!(
f,
"ios-{}",
match arch {
Architecture::ARM64 => "arm64",
_ => "unknown",
}
),
Platform::MacOS { arch } => write!(
f,
"darwin-{}",
match arch {
Architecture::ARM64 => "aarch64",
Architecture::X86 => "x86",
Architecture::X86_64 => "x86_64",
_ => "unknown",
}
),
Platform::Windows { arch } => write!(
f,
"windows-{}",
match arch {
Architecture::ARM64 => "aarch64",
Architecture::X86 => "x86",
Architecture::X86_64 => "x86_64",
_ => "unknown",
}
),
Platform::Linux { arch } => write!(
f,
"linux-{}",
match arch {
Architecture::ARM64 => "aarch64",
Architecture::X86 => "x86",
Architecture::X86_64 => "x86_64",
_ => "unknown",
}
),
Platform::Web => write!(f, "web"),
}
}
}
#[derive(Debug, Clone)]
pub enum Architecture {
ARMv7,
ARM64,
X86,
X86_64,
}
impl Display for Architecture {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Architecture::ARMv7 => write!(f, "armeabi-v7a"),
Architecture::ARM64 => write!(f, "arm64-v8a"),
Architecture::X86 => write!(f, "x86"),
Architecture::X86_64 => write!(f, "x86_64"),
}
}
}
#[derive(Debug, Clone)]
/// A local/remote location where the artifact is located
pub enum RepoResource {
Remote(String),
Local(PathBuf),
}
#[derive(Debug, Clone)]
/// A single release with one or more artifacts
pub struct RepoRelease {
/// Release version (semver)
pub version: Version,
/// Release changelog/notes
pub description: Option<String>,
/// URL of the release (github release page etc)
pub url: Option<String>,
/// List of artifacts in this release
pub artifacts: Vec<RepoArtifact>,
}
impl RepoRelease {
pub fn app_id(&self) -> Result<String> {
self.artifacts
.iter()
.find_map(|a| match &a.metadata {
ArtifactMetadata::APK { manifest, .. } if manifest.package.is_some() => {
Some(manifest.package.as_ref().unwrap().to_string())
}
_ => None,
})
.ok_or(anyhow!("no app_id found"))
}
/// [app_id]@[version]
pub fn release_tag(&self) -> Result<String> {
Ok(format!("{}@{}", self.app_id()?, self.version))
}
/// Create nostr release artifact list event
pub async fn into_release_list_event<T: NostrSigner>(
self,
signer: &T,
app_coord: Coordinate,
) -> Result<Vec<Event>> {
let mut ret = vec![];
let mut b = EventBuilder::new(
Kind::Custom(30063),
self.description.as_deref().unwrap_or(""),
)
.tags([
Tag::coordinate(app_coord),
Tag::parse(["d", &self.release_tag()?])?,
]);
if let Some(url) = self.url {
b = b.tag(Tag::parse(["url", &url])?);
}
for a in &self.artifacts {
let eb: Result<EventBuilder> = a.clone().try_into();
match eb {
Ok(a) => {
let e_build = a.sign(signer).await?;
b = b.tag(Tag::event(e_build.id));
ret.push(e_build);
}
Err(e) => warn!("Failed to convert artifact: {} {}", a, e),
}
}
ret.push(b.sign(signer).await?);
Ok(ret)
}
}
/// Generic artifact repository
#[async_trait::async_trait]
pub trait Repo {
/// Get a list of release artifacts
async fn get_releases(&self) -> Result<Vec<RepoRelease>>;
}
impl TryInto<Box<dyn Repo>> for &Manifest {
type Error = anyhow::Error;
fn try_into(self) -> std::result::Result<Box<dyn Repo>, Self::Error> {
let repo = self
.repository
.as_ref()
.ok_or(anyhow!("repository not found"))?;
if !repo.starts_with("https://github.com/") {
bail!("Only github repos are supported");
}
Ok(Box::new(GithubRepo::from_url(repo)?))
}
}
/// Download an artifact and create a [RepoArtifact]
async fn load_artifact_url(url: &str) -> Result<RepoArtifact> {
info!("Downloading artifact {}", url);
let u = Url::parse(url)?;
let rsp = reqwest::get(u.clone()).await?;
let id = hex::encode(sha2::Sha256::digest(url.as_bytes()));
let mut tmp = temp_dir().join(id);
tmp.set_extension(
PathBuf::from(u.path())
.extension()
.ok_or(anyhow!("Missing extension in URL"))?
.to_str()
.unwrap(),
);
if !tmp.exists() {
let mut tmp_file = tokio::fs::File::create(&tmp).await?;
let mut rsp_stream = rsp.bytes_stream();
while let Some(data) = rsp_stream.next().await {
if let Ok(data) = data {
tmp_file.write_all(&data).await?;
}
}
}
let mut a = load_artifact(&tmp)?;
// replace location back to URL for publishing
a.location = RepoResource::Remote(url.to_string());
Ok(a)
}
fn load_artifact(path: &Path) -> Result<RepoArtifact> {
match path
.extension()
.ok_or(anyhow!("missing file extension"))?
.to_str()
.unwrap()
{
"apk" => load_apk_artifact(path),
v => bail!("unknown file extension: {v}"),
}
}
fn load_apk_artifact(path: &Path) -> Result<RepoArtifact> {
let file = std::fs::File::open(path)?;
let mut file = std::io::BufReader::new(file);
let sig_block = load_signing_block(&mut file)?;
let mut zip = ZipArchive::new(file)?;
let manifest = load_manifest(&mut zip)?;
let lib_arch: HashSet<String> = list_libs(&mut zip)
.iter()
.filter_map(|p| {
PathBuf::from(p)
.iter()
.nth(1)
.map(|p| p.to_str().unwrap().to_owned())
})
.collect();
ensure!(lib_arch.len() == 1, "Unknown library architecture");
Ok(RepoArtifact {
name: path.file_name().unwrap().to_str().unwrap().to_string(),
size: path.metadata()?.len(),
location: RepoResource::Local(path.to_path_buf()),
hash: Some(hash_file(path)?),
content_type: "application/vnd.android.package-archive".to_string(),
platform: Platform::Android {
arch: match lib_arch.iter().next().unwrap().as_str() {
"arm64-v8a" => Architecture::ARM64,
"armeabi-v7a" => Architecture::ARMv7,
"x86_64" => Architecture::X86_64,
"x86" => Architecture::X86,
v => bail!("unknown architecture: {v}"),
},
},
metadata: ArtifactMetadata::APK {
manifest,
signature: sig_block.try_into()?,
},
})
}
fn hash_file(path: &Path) -> Result<Vec<u8>> {
let mut file = File::open(path)?;
let mut hash = Sha256::default();
let mut buf = Vec::with_capacity(4096);
while let Ok(r) = file.read(&mut buf) {
if r == 0 {
break;
}
hash.update(&buf[..r]);
}
Ok(hash.finalize().to_vec())
}
fn load_manifest<T>(zip: &mut ZipArchive<T>) -> Result<AndroidManifest>
where
T: Read + Seek,
{
const ANDROID_MANIFEST: &str = "AndroidManifest.xml";
let mut f = zip.by_name(ANDROID_MANIFEST)?;
let mut manifest_data = Vec::with_capacity(8192);
let r = f.read_to_end(&mut manifest_data)?;
let res: AndroidManifest = parse_android_manifest(&manifest_data[..r])?;
Ok(res)
}
#[derive(Debug, Clone)]
struct ApkSigningBlock {
pub data: Vec<(u32, Vec<u8>)>,
}
impl TryInto<ApkSignatureBlock> for ApkSigningBlock {
type Error = anyhow::Error;
fn try_into(self) -> std::result::Result<ApkSignatureBlock, Self::Error> {
const V2_SIG_BLOCK_ID: u32 = 0x7109871a;
const V3_SIG_BLOCK_ID: u32 = 0xf05368c0;
if let Some(v3) =
self.data
.iter()
.find_map(|(k, v)| if *k == V3_SIG_BLOCK_ID { Some(v) } else { None })
{
todo!("Not done yet")
}
if let Some(v2) =
self.data
.iter()
.find_map(|(k, v)| if *k == V2_SIG_BLOCK_ID { Some(v) } else { None })
{
let v2 = get_length_prefixed_u32_sequence(&v2[4..])?;
let signed_data = get_sequence(v2[0])?;
let digests = get_sequence_kv(signed_data[0])?;
let certificates = get_sequence(signed_data[1])?;
let attributes = get_sequence_kv(signed_data[2])?;
let signatures = get_sequence_kv(v2[1])?;
let public_key = v2[2];
let digests: HashMap<u32, &[u8]> = HashMap::from_iter(digests);
return Ok(ApkSignatureBlock::V2 {
attributes: HashMap::from_iter(
attributes.into_iter().map(|(k, v)| (k, v.to_vec())),
),
certificates: certificates.into_iter().map(|v| v.to_vec()).collect(),
signatures: signatures
.into_iter()
.filter_map(|(k, v)| {
let sig_len = u32::from_le_bytes(v[..4].try_into().ok()?) as usize;
if sig_len > v.len() - 4 {
warn!("Invalid signature length: {} > {}", sig_len, v.len());
return None;
}
if let Ok(a) = ApkSignatureAlgo::try_from(k) {
Some(ApkSignature {
algo: a,
digest: digests.get(&k).map(|v| v[4..].to_vec())?,
signature: v[4..sig_len + 4].to_vec(),
})
} else {
None
}
})
.collect(),
public_key: public_key.to_vec(),
});
}
Ok(ApkSignatureBlock::None)
}
}
fn load_signing_block<R>(zip: &mut R) -> Result<ApkSigningBlock>
where
R: Read + Seek,
{
const SIG_BLOCK_MAGIC: &[u8] = b"APK Sig Block 42";
// scan backwards until we find the singing block
let flen = zip.seek(SeekFrom::End(0))?;
let mut magic_buf = [0u8; 16];
loop {
let magic_pos = zip.seek(SeekFrom::Current(-17))?;
if magic_pos <= 4 {
bail!("Failed to find signing block");
}
zip.read_exact(&mut magic_buf)?;
if magic_buf == SIG_BLOCK_MAGIC {
zip.seek(SeekFrom::Current(-(16 + 8)))?;
let size1 = zip.read_u64::<LittleEndian>()?;
ensure!(size1 <= flen, "Signing block is larger than entire file");
zip.seek(SeekFrom::Current(-(size1 as i64 - 8)))?;
let size2 = zip.read_u64::<LittleEndian>()?;
ensure!(
size2 == size1,
"Invalid block sizes, {} != {}",
size1,
size2
);
let mut data_bytes = size1 - 8 - 16;
let mut sigs = Vec::new();
loop {
let (k, v) = read_u64_length_prefixed_kv(zip)?;
data_bytes -= (v.len() + 4 + 8) as u64;
sigs.push((k, v));
if data_bytes == 0 {
break;
}
}
zip.seek(SeekFrom::Start(0))?;
return Ok(ApkSigningBlock { data: sigs });
}
}
}
#[inline]
fn read_u64_length_prefixed_kv<T>(file: &mut T) -> Result<(u32, Vec<u8>)>
where
T: Read + Seek,
{
let kv_len = file.read_u64::<LittleEndian>()?;
let k = file.read_u32::<LittleEndian>()?;
let v_len = kv_len as usize - 4;
let mut v = vec![0; v_len];
file.read_exact(v.as_mut_slice())?;
Ok((k, v))
}
#[inline]
fn get_u64_length_prefixed_kv(slice: &[u8]) -> Result<(u32, &[u8])> {
let kv_len = u64::from_le_bytes(slice[..8].try_into()?);
let k = u32::from_le_bytes(slice[8..12].try_into()?);
Ok((k, &slice[12..(kv_len as usize - 12)]))
}
#[inline]
fn get_u32_length_prefixed_kv(slice: &[u8]) -> Result<(u32, &[u8])> {
let kv_len = u32::from_le_bytes(slice[..4].try_into()?);
let k = u32::from_le_bytes(slice[4..8].try_into()?);
Ok((k, &slice[8..(kv_len as usize - 8)]))
}
#[inline]
fn get_length_prefixed_u32(slice: &[u8]) -> Result<&[u8]> {
let len = u32::from_le_bytes(slice[..4].try_into()?);
Ok(&slice[4..4 + len as usize])
}
#[inline]
fn get_length_prefixed_u32_sequence(slice: &[u8]) -> Result<Vec<&[u8]>> {
let sequence_len = u32::from_le_bytes(slice[..4].try_into()?);
get_sequence(&slice[4..4 + sequence_len as usize])
}
#[inline]
fn get_sequence(mut slice: &[u8]) -> Result<Vec<&[u8]>> {
let mut ret = Vec::new();
while slice.len() >= 4 {
let data = get_length_prefixed_u32(slice)?;
let r_len = data.len() + 4;
slice = &slice[r_len..];
ret.push(data);
}
Ok(ret)
}
#[inline]
fn get_sequence_kv(slice: &[u8]) -> Result<Vec<(u32, &[u8])>> {
let seq = get_sequence(slice)?;
Ok(seq
.into_iter()
.map(|s| {
let k = u32::from_le_bytes(s[..4].try_into().unwrap());
let v = &s[4..];
(k, v)
})
.collect())
}
fn list_libs<T>(zip: &mut ZipArchive<T>) -> Vec<String>
where
T: Read + Seek,
{
zip.file_names()
.filter_map(|f| {
if f.starts_with("lib/") {
Some(f.to_string())
} else {
None
}
})
.collect()
}
fn parse_android_manifest(data: &[u8]) -> Result<AndroidManifest> {
let chunks = if let Chunk::Xml(chunks) = Chunk::parse(&mut Cursor::new(data))? {
chunks
} else {
bail!("Invalid AndroidManifest file");
};
let strings = if let Chunk::StringPool(strings, _) = &chunks[0] {
HashMap::from_iter(
strings
.iter()
.enumerate()
.map(|(i, s)| (s.to_string(), i as i32)),
)
} else {
bail!("invalid manifest 1");
};
let mut res = AndroidManifest::default();
res.package = find_value_in(&strings, &chunks, "manifest", "package");
res.version_code =
find_value_in(&strings, &chunks, "manifest", "versionCode").and_then(|v| v.parse().ok());
res.version_name = find_value_in(&strings, &chunks, "manifest", "versionName");
res.compile_sdk_version = find_value_in(&strings, &chunks, "manifest", "compileSdkVersion")
.and_then(|v| v.parse().ok());
res.compile_sdk_version_codename =
find_value_in(&strings, &chunks, "manifest", "compileSdkVersionCodename")
.and_then(|v| v.parse().ok());
res.platform_build_version_code =
find_value_in(&strings, &chunks, "manifest", "platformBuildVersionCode")
.and_then(|v| v.parse().ok());
res.platform_build_version_name =
find_value_in(&strings, &chunks, "manifest", "platformBuildVersionName")
.and_then(|v| v.parse().ok());
res.sdk.min_sdk_version =
find_value_in(&strings, &chunks, "uses-sdk", "minSdkVersion").and_then(|v| v.parse().ok());
res.sdk.target_sdk_version = find_value_in(&strings, &chunks, "uses-sdk", "targetSdkVersion")
.and_then(|v| v.parse().ok());
res.sdk.max_sdk_version =
find_value_in(&strings, &chunks, "uses-sdk", "maxSdkVersion").and_then(|v| v.parse().ok());
res.application.theme = find_value_in(&strings, &chunks, "application", "theme");
res.application.label = find_value_in(&strings, &chunks, "application", "label");
res.application.icon = find_value_in(&strings, &chunks, "application", "icon");
Ok(res)
}
fn find_value_in(
strings: &HashMap<String, i32>,
chunks: &Vec<Chunk>,
node: &str,
attr: &str,
) -> Option<String> {
let idx_node = if let Some(i) = strings.get(node) {
*i
} else {
return None;
};
let idx_attr = if let Some(i) = strings.get(attr) {
*i
} else {
return None;
};
chunks.iter().find_map(|chunk| {
if let Chunk::XmlStartElement(_, el, attrs) = chunk {
match el.name {
x if x == idx_node => attrs.iter().find(|e| e.name == idx_attr).and_then(|e| {
debug!("{}, {}, {:?}", node, attr, e);
match e.typed_value.data_type {
3 => strings
.iter()
.find(|(_, v)| **v == e.raw_value)
.map(|(k, _)| k.clone()),
16 => Some(e.typed_value.data.to_string()),
_ => {
debug!("unknown data type {},{},{:?}", node, attr, e);
None
}
}
}),
_ => None,
}
} else {
None
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[ignore]
#[test]
fn read_apk() -> Result<()> {
let path = "/home/kieran/Downloads/app-arm64-v8a-release.apk";
let apk = load_apk_artifact(&PathBuf::from(path))?;
assert!(
matches!(&apk.platform, Platform::Android { arch } if matches!(arch, Architecture::ARM64 { .. }))
);
assert!(matches!(&apk.metadata,
ArtifactMetadata::APK { signature, .. } if matches!(signature,
ApkSignatureBlock::V2 { signatures, .. } if signatures.len() == 1 &&
matches!(signatures[0].algo, ApkSignatureAlgo::RsaSsaPkcs1Sha256))));
eprint!("{}", apk);
Ok(())
}
}