diff --git a/lib/gofer/Cargo.toml b/lib/gofer/Cargo.toml index 4a61967..d769cb1 100644 --- a/lib/gofer/Cargo.toml +++ b/lib/gofer/Cargo.toml @@ -23,15 +23,15 @@ std = [ "percent-encoding?/std", "thiserror/std", ] -all = ["data", "file", "ftp", "http", "https", "ipfs", "stdin"] -unstable = ["ftps", "git", "scp"] +all = ["data", "file", "ftp", "git", "http", "https", "ipfs", "stdin"] +unstable = ["ftps", "scp"] # Protocols: data = ["dep:data-url"] file = ["std"] ftp = ["dep:percent-encoding", "dep:suppaftp"] ftps = ["ftp", "suppaftp?/rustls"] -git = ["dep:gix-protocol"] +git = ["dep:reqwest", "reqwest?/blocking"] http = ["dep:reqwest", "reqwest?/blocking"] https = ["http", "reqwest?/default", "reqwest?/rustls-tls-native-roots"] ipfs = ["dep:reqwest", "reqwest?/blocking"] @@ -56,9 +56,6 @@ dogma = { version = "0.1.5", default-features = false, features = [ "iri", "uri", ] } -gix-protocol = { version = "0.49", default-features = false, features = [ - "blocking-client", -], optional = true } miette = { version = "7.5", default-features = false, features = [ "derive", ], optional = true } diff --git a/lib/gofer/examples/git.rs b/lib/gofer/examples/git.rs new file mode 100644 index 0000000..4f11644 --- /dev/null +++ b/lib/gofer/examples/git.rs @@ -0,0 +1,11 @@ +// This is free and unencumbered software released into the public domain. + +use std::{boxed::Box, error::Error, io::stdout, result::Result}; + +pub fn main() -> Result<(), Box> { + let mut output = stdout().lock(); + let mut input = gofer::open("git://github.com/dryrust/gofer.rs/master/VERSION")?; + std::io::copy(&mut input, &mut output)?; + + Ok(()) +} diff --git a/lib/gofer/src/error.rs b/lib/gofer/src/error.rs index 4e7b83d..85beaed 100644 --- a/lib/gofer/src/error.rs +++ b/lib/gofer/src/error.rs @@ -113,6 +113,18 @@ pub enum Error { )] FailedHttpRequest(#[from] reqwest::Error), + #[cfg(feature = "git")] + #[error("invalid Git URL: {0}")] + #[cfg_attr( + feature = "miette", + diagnostic( + code(gofer::invalid_git_url), + help("it seems that the URL is malformed in some way"), + url(docsrs), + ) + )] + InvalidGitUrl(String), + #[cfg(feature = "ipfs")] #[error("invalid IPFS URL: {0}")] #[cfg_attr( @@ -155,6 +167,9 @@ impl From for std::io::Error { #[cfg(any(feature = "http", feature = "https"))] Error::FailedHttpRequest(_e) => std::io::Error::from(ErrorKind::Other), // FIXME + #[cfg(feature = "git")] + Error::InvalidGitUrl(u) => std::io::Error::new(ErrorKind::InvalidInput, u.as_str()), + #[cfg(feature = "ipfs")] Error::InvalidIpfsUrl(u) => std::io::Error::new(ErrorKind::InvalidInput, u.as_str()), } diff --git a/lib/gofer/src/schemes/git.rs b/lib/gofer/src/schemes/git.rs index 2518356..ddcb025 100644 --- a/lib/gofer/src/schemes/git.rs +++ b/lib/gofer/src/schemes/git.rs @@ -1,9 +1,103 @@ // This is free and unencumbered software released into the public domain. -use crate::{Read, Result, Url}; +use crate::{Error, Read, Result, Url}; +use reqwest::{blocking::ClientBuilder, redirect}; -/// See: https://en.wikipedia.org/wiki/Git +static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); + +/// Downloads a file from a git repository. +/// +/// Supports: +/// - GitHub: git://github.com/owner/repo/branch/...path +/// - GitLab: git://gitlab.com/owner/repo/branch/...path +/// /// See: https://git-scm.com/docs/protocol-v2 -pub fn open<'a, 'b>(_url: &'a Url<'b>) -> Result> { - todo!() // TODO +/// See: https://git-scm.com/docs/gitweb +/// See: https://docs.github.com/en/repositories/working-with-files/using-files/getting-permanent-links-to-files +pub fn open<'a, 'b>(url: &'a Url<'b>) -> Result> { + // See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.ClientBuilder.html + let client = ClientBuilder::new() + .user_agent(USER_AGENT) + .redirect(redirect::Policy::default()) + .https_only(true); + + let url = map_git_url_to_raw_url(url.as_str())?; + + // See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.Client.html#method.get + // See: https://docs.rs/reqwest/latest/reqwest/blocking/struct.RequestBuilder.html + let response = client.build()?.get(url).send()?; + + Ok(Box::new(response)) +} + +/// Maps a git URL to a raw content URL for supported git providers. +/// - GitHub: git://github.com/owner/repo/branch/...path -> https://raw.githubusercontent.com/owner/repo/refs/heads/branch/...path +/// - GitLab: git://gitlab.com/owner/repo/branch/...path -> https://gitlab.com/owner/repo/-/raw/branch/...path +fn map_git_url_to_raw_url(url_str: &str) -> Result { + let Some(path) = url_str.strip_prefix("git://") else { + return Err(Error::InvalidGitUrl(format!( + "Invalid git URL format, expected `git://`: {}", + url_str + ))); + }; + + let components: Vec<&str> = path.split('/').collect(); + + let host = components + .first() + .ok_or_else(|| Error::InvalidGitUrl(format!("Invalid git URL format: {}", url_str)))?; + + if components.len() < 5 { + return Err(Error::InvalidGitUrl(format!( + "Invalid GitHub git URL format (need at least 5 components): {}", + url_str + ))); + } + + let owner = components[1]; + let repo = components[2]; + let version = components[3]; + let file_path = components[4..].join("/"); + + match *host { + "github.com" => Ok(format!( + "https://raw.githubusercontent.com/{}/{}/{}/{}", + owner, repo, version, file_path + )), + "gitlab.com" => Ok(format!( + "https://gitlab.com/{}/{}/-/raw/{}/{}", + owner, repo, version, file_path + )), + _ => Err(Error::InvalidGitUrl(format!( + "Unsupported git host: {}", + host + ))), + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn url_mapping() { + assert_eq!( + map_git_url_to_raw_url( + "git://github.com/dryrust/gofer.rs/master/lib/gofer/src/schemes/git.rs" + ) + .unwrap(), + "https://raw.githubusercontent.com/dryrust/gofer.rs/master/lib/gofer/src/schemes/git.rs" + ); + assert_eq!( + map_git_url_to_raw_url( + "git://github.com/dryrust/gofer.rs/f4ea4a585c009aefd570cefcb6062dc5d579c6ab/VERSION" + ) + .unwrap(), + "https://raw.githubusercontent.com/dryrust/gofer.rs/f4ea4a585c009aefd570cefcb6062dc5d579c6ab/VERSION" + ); + assert_eq!( + map_git_url_to_raw_url("git://gitlab.com/rust-lang/rust/master/src/README.md").unwrap(), + "https://gitlab.com/rust-lang/rust/-/raw/master/src/README.md" + ) + } }