use std::io::Read;
use crate::ComposeError;
use super::ReleaseSource;
pub const REPO: &str = "Glyndor/podup";
const MAX_ASSET_BYTES: u64 = 128 * 1024 * 1024;
const MAX_METADATA_BYTES: u64 = 1024 * 1024;
pub struct GitHubSource {
repo: String,
agent: ureq::Agent,
}
impl GitHubSource {
pub fn new(repo: impl Into<String>) -> Self {
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_global(Some(std::time::Duration::from_secs(60)))
.user_agent(concat!("podup/", env!("CARGO_PKG_VERSION")))
.build()
.into();
Self {
repo: repo.into(),
agent,
}
}
}
impl Default for GitHubSource {
fn default() -> Self {
Self::new(REPO)
}
}
fn read_capped(mut reader: impl Read, cap: u64) -> crate::Result<Vec<u8>> {
let mut buf = Vec::new();
let read = reader
.by_ref()
.take(cap + 1)
.read_to_end(&mut buf)
.map_err(ComposeError::Io)?;
if read as u64 > cap {
return Err(ComposeError::Update(
"release data exceeds the maximum allowed size".to_string(),
));
}
Ok(buf)
}
impl ReleaseSource for GitHubSource {
fn latest_version(&self) -> crate::Result<String> {
let url = format!("https://api.github.com/repos/{}/releases/latest", self.repo);
let resp = self
.agent
.get(&url)
.header("Accept", "application/vnd.github+json")
.call()
.map_err(|e| ComposeError::Update(format!("cannot reach GitHub releases API: {e}")))?;
let body = read_capped(resp.into_body().into_reader(), MAX_METADATA_BYTES)?;
#[derive(serde::Deserialize)]
struct Latest {
tag_name: String,
}
let latest: Latest = serde_json::from_slice(&body)
.map_err(|e| ComposeError::Update(format!("malformed release metadata: {e}")))?;
Ok(latest.tag_name)
}
fn fetch(&self, asset: &str) -> crate::Result<Vec<u8>> {
let url = format!(
"https://github.com/{}/releases/latest/download/{asset}",
self.repo
);
let resp = self
.agent
.get(&url)
.call()
.map_err(|e| ComposeError::Update(format!("download failed for {asset}: {e}")))?;
read_capped(resp.into_body().into_reader(), MAX_ASSET_BYTES)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Endless;
impl Read for Endless {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
for b in buf.iter_mut() {
*b = 0;
}
Ok(buf.len())
}
}
#[test]
fn read_capped_accepts_small() {
let data = b"hello world".to_vec();
let got = read_capped(&data[..], MAX_ASSET_BYTES).unwrap();
assert_eq!(got, data);
}
#[test]
fn read_capped_rejects_oversize() {
assert!(read_capped(Endless, MAX_ASSET_BYTES).is_err());
}
#[test]
fn read_capped_enforces_metadata_cap() {
assert!(read_capped(Endless, MAX_METADATA_BYTES).is_err());
}
#[test]
fn read_capped_accepts_up_to_cap() {
let exactly = [0u8; 8];
assert!(read_capped(&exactly[..], 8).is_ok());
let over = [0u8; 9];
assert!(read_capped(&over[..], 8).is_err());
}
#[test]
fn default_uses_canonical_repo() {
let src = GitHubSource::default();
assert_eq!(src.repo, REPO);
}
}