securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use crate::auth::SecureString;
use crate::lfs::pointer::LfsPointer;
use anyhow::{bail, Context, Result};
use reqwest::Client;
use sha2::{Digest, Sha256};
use std::path::Path;

/// Downloads LFS objects via the batch API, verifying SHA-256.
pub struct LfsDownloader {
    client: Client,
}

impl LfsDownloader {
    pub fn new() -> Self {
        Self {
            client: Client::builder()
                .timeout(std::time::Duration::from_secs(600))
                .build()
                .expect("failed to build HTTP client"),
        }
    }

    /// Download an LFS object and verify its hash.
    pub async fn download_object(
        &self,
        repo_url: &str,
        pointer: &LfsPointer,
        dest: &Path,
        token: Option<&SecureString>,
    ) -> Result<()> {
        let lfs_url = format!(
            "{}/info/lfs/objects/batch",
            repo_url.trim_end_matches(".git")
        );

        // Build batch request
        let body = serde_json::json!({
            "operation": "download",
            "transfers": ["basic"],
            "objects": [{
                "oid": pointer.oid,
                "size": pointer.size,
            }]
        });

        let mut req = self
            .client
            .post(&lfs_url)
            .header("Content-Type", "application/vnd.git-lfs+json")
            .header("Accept", "application/vnd.git-lfs+json")
            .json(&body);

        if let Some(tok) = token {
            req = req.header("Authorization", format!("token {}", tok.as_str()));
        }

        let response = req.send().await.context("LFS batch request failed")?;

        if !response.status().is_success() {
            bail!("LFS batch API returned HTTP {}", response.status());
        }

        let batch_resp: serde_json::Value = response.json().await?;

        // Extract download URL from batch response
        let download_url = batch_resp["objects"][0]["actions"]["download"]["href"]
            .as_str()
            .ok_or_else(|| anyhow::anyhow!("No download URL in LFS batch response"))?;

        // Validate HTTPS only
        if !download_url.starts_with("https://") {
            bail!("LFS download URL must be HTTPS, got: {}", download_url);
        }

        // Download the actual object
        let obj_response = self
            .client
            .get(download_url)
            .send()
            .await
            .context("LFS object download failed")?;

        if !obj_response.status().is_success() {
            bail!(
                "LFS object download returned HTTP {}",
                obj_response.status()
            );
        }

        let bytes = obj_response.bytes().await?;

        // Verify SHA-256
        let mut hasher = Sha256::new();
        hasher.update(&bytes);
        let hash = hex::encode(hasher.finalize());

        if hash != pointer.oid {
            bail!(
                "LFS object hash mismatch: expected {}, got {}",
                pointer.oid,
                hash
            );
        }

        // Write verified object
        tokio::fs::write(dest, &bytes).await?;

        Ok(())
    }
}

impl Default for LfsDownloader {
    fn default() -> Self {
        Self::new()
    }
}