use crate::verifier::egress::GatewayFetcher;
use crate::verifier::fetch::{FetchOutboundOptions, HttpMethod, HttpPurpose};
use crate::verifier::types::{UriCheck, UriFailureReason};
const ARWEAVE_DEFAULTS: [&str; 3] = [
"https://arweave.net",
"https://ar-io.net",
"https://g8way.io",
];
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FetchItemError {
#[error("URI_TARGET_FORBIDDEN: no in-set URI scheme in uris[]")]
UriTargetForbidden,
#[error("CONTENT_UNAVAILABLE: {0}")]
ContentUnavailable(String),
}
pub fn fetch_item_ciphertext(
uris: &[Vec<String>],
fetcher: &mut GatewayFetcher<'_>,
uri_checks_out: &mut Vec<UriCheck>,
item_index: i64,
arweave_gateways: Option<&[String]>,
ipfs_gateways: Option<&[String]>,
) -> Result<Vec<u8>, FetchItemError> {
let reconstructed: Vec<String> = uris.iter().map(|chunks| chunks.concat()).collect();
let candidate = reconstructed
.iter()
.find(|u| u.starts_with("ar://") || u.starts_with("ipfs://"));
let Some(candidate) = candidate.cloned() else {
for u in &reconstructed {
uri_checks_out.push(UriCheck {
item_index,
uri: u.clone(),
ok: false,
reason: Some(UriFailureReason::UriTargetForbidden),
});
}
return Err(FetchItemError::UriTargetForbidden);
};
if let Some(txid) = candidate.strip_prefix("ar://") {
if !is_arweave_txid(txid) {
uri_checks_out.push(UriCheck {
item_index,
uri: candidate.clone(),
ok: false,
reason: Some(UriFailureReason::ContentUnavailable),
});
return Err(FetchItemError::ContentUnavailable(format!(
"malformed arweave txid: {txid}"
)));
}
let default_gateways: Vec<String> =
ARWEAVE_DEFAULTS.iter().map(|s| (*s).to_string()).collect();
let gateways = match arweave_gateways {
Some(g) if !g.is_empty() => g,
_ => &default_gateways,
};
for gw in gateways {
let opts = FetchOutboundOptions::new(HttpMethod::Get, HttpPurpose::Arweave);
match fetcher.fetch(&format!("{gw}/{txid}"), &opts) {
Ok(res) if res.status == 200 => {
uri_checks_out.push(UriCheck {
item_index,
uri: candidate.clone(),
ok: true,
reason: None,
});
return Ok(res.bytes);
}
Ok(_) | Err(_) => uri_checks_out.push(UriCheck {
item_index,
uri: candidate.clone(),
ok: false,
reason: Some(UriFailureReason::UriFetchFailed),
}),
}
}
return Err(FetchItemError::ContentUnavailable(
"all arweave gateways exhausted".to_string(),
));
}
let cid_part = candidate.trim_start_matches("ipfs://");
let ipfs_cid = cid_part
.split('/')
.next()
.filter(|s| !s.is_empty())
.unwrap_or(cid_part);
let gateways = match ipfs_gateways {
Some(g) if !g.is_empty() => g,
_ => {
uri_checks_out.push(UriCheck {
item_index,
uri: candidate.clone(),
ok: false,
reason: Some(UriFailureReason::ContentUnavailable),
});
return Err(FetchItemError::ContentUnavailable(
"no ipfs gateway configured".to_string(),
));
}
};
for gw in gateways {
let sep = if gw.ends_with('/') { "" } else { "/" };
let url = format!("{gw}{sep}ipfs/{ipfs_cid}");
let opts = FetchOutboundOptions::new(HttpMethod::Get, HttpPurpose::Ipfs);
match fetcher.fetch(&url, &opts) {
Ok(res) if res.status == 200 => {
uri_checks_out.push(UriCheck {
item_index,
uri: candidate.clone(),
ok: true,
reason: None,
});
return Ok(res.bytes);
}
Ok(_) | Err(_) => uri_checks_out.push(UriCheck {
item_index,
uri: candidate.clone(),
ok: false,
reason: Some(UriFailureReason::UriFetchFailed),
}),
}
}
Err(FetchItemError::ContentUnavailable(
"all ipfs gateways exhausted".to_string(),
))
}
fn is_arweave_txid(txid: &str) -> bool {
txid.len() == 43
&& txid
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
}