diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..3689eaf --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,24 @@ +changelog: + exclude: + labels: + - ignore-for-release-notes + - CI + - CD + - test + - docs + categories: + - title: Breaking Changes 🛠 + labels: + - Semver-Major + - Semver-Minor + - breaking-change + - title: Exciting New Features 🎉 + labels: + - Semver-Patch + - enhancement + - title: Bug Fixes + labels: + - bugfix + - title: Other Changes + labels: + - "*" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..70c1998 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: git-url-parse + +on: + push: + branches: + - staging + - trying + pull_request: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + ci: + name: CI + runs-on: ubuntu-latest + strategy: + matrix: + cargo_checks: + - name: Enforce default cargo fmt + subcommand: fmt -- --check + - name: Clippy + subcommand: clippy + - name: Test + subcommand: test --verbose + - name: Build + subcommand: build --release --all-features --verbose + steps: + - uses: actions/checkout@v2 + - name: Stable with rustfmt and clippy + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v1 + - name: cargo check - ${{ matrix.cargo_checks.name }} + run: cargo ${{ matrix.cargo_checks.subcommand }} + done: + name: Done + if: github.event_name == 'push' && github.ref == 'refs/heads/staging' + needs: [ci] + runs-on: ubuntu-latest + steps: + - name: Done + run: echo "Done!" \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f52bbd6 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,20 @@ +name: Publish + +on: +# push: +# branches: [master] + workflow_dispatch: + +jobs: + publish: + name: Publish to crates.io + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - uses: katyo/publish-crates@v1 + with: + registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 8c295bd..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: git-url-parse - -on: [push] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7894a..28fc58b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [0.4.0](https://github.com/tjtelan/git-url-parse-rs/compare/v0.3.1...v0.4.0) +- Migrate to Rust 2021 +- Check for null bytes within input url before parsing (+ adding tests from [#16](https://github.com/tjtelan/git-url-parse-rs/issues/16)) +- Replace `anyhow` with `color-eyre` +- Replace panic behavior with returning `Err()` +- Update dependencies +- Clippy/rustfmt fixes + add clippy/rustfmt checks to CI + # [0.3.1](https://github.com/tjtelan/git-url-parse-rs/compare/v0.3.0...v0.3.1) - Loosen dependency restrictions in `Cargo.toml` ([#12](https://github.com/tjtelan/git-url-parse-rs/issues/12)) - Update `strum` + `strum_macros` ([#14](https://github.com/tjtelan/git-url-parse-rs/issues/14)) diff --git a/Cargo.toml b/Cargo.toml index 48f3b59..2fbc0e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,24 +3,21 @@ authors = ["T.J. Telan "] categories = ["parser-implementations", "encoding"] description = "A parser for git repo urls based on url crate" documentation = "https://docs.rs/git-url-parse" -edition = "2018" +edition = "2021" keywords = ["git", "url", "parsing"] license = "MIT" name = "git-url-parse" readme = "README.md" repository = "https://github.com/tjtelan/git-url-parse-rs" -version = "0.3.1" - -[badges.maintenance] -status = "actively-developed" +version = "0.4.0" [dependencies] log = "^0.4" url = "^2.2" -strum = "^0.20" -strum_macros = "^0.20" -anyhow = "^1.0" +strum = "^0.22" +strum_macros = "^0.22" +color-eyre = "^0.5" regex = "^1.4" [dev-dependencies] -env_logger = "^0.8" +env_logger = "^0.9" diff --git a/README.md b/README.md index 110d7ea..f5bf531 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # git-url-parse [![Crates.io](https://img.shields.io/crates/v/git-url-parse)](https://crates.io/crates/git-url-parse) +![Crates.io](https://img.shields.io/crates/d/git-url-parse) +[![Github actions build status](https://github.com/tjtelan/git-url-parse-rs/workflows/git-url-parse/badge.svg)](https://github.com/tjtelan/git-url-parse-rs/actions/workflows/rust.yml) [![docs.rs](https://docs.rs/git-url-parse/badge.svg)](https://docs.rs/git-url-parse/) [![licence](https://img.shields.io/github/license/tjtelan/git-url-parse-rs)](LICENSE) -![Github actions build status](https://github.com/tjtelan/git-url-parse-rs/workflows/git-url-parse/badge.svg) +![Maintenance](https://img.shields.io/maintenance/yes/2021) Supports common protocols as specified by the [Pro Git book](https://git-scm.com/book/en/v2) diff --git a/bors.toml b/bors.toml new file mode 100644 index 0000000..450460e --- /dev/null +++ b/bors.toml @@ -0,0 +1,6 @@ +status = [ + "Done", +] +use_squash_merge = true +delete_merged_branches = true +timeout_sec = 900 # 15 mins \ No newline at end of file diff --git a/examples/multi.rs b/examples/multi.rs index 8da8fd6..4cc9d44 100644 --- a/examples/multi.rs +++ b/examples/multi.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use color_eyre::Result; use git_url_parse::GitUrl; fn main() -> Result<()> { diff --git a/examples/trim_auth.rs b/examples/trim_auth.rs index 03d2b6a..1b85967 100644 --- a/examples/trim_auth.rs +++ b/examples/trim_auth.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use color_eyre::Result; use git_url_parse::GitUrl; fn main() -> Result<()> { diff --git a/src/lib.rs b/src/lib.rs index dcb6e00..61002e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -use anyhow::Result; +use color_eyre::eyre::{eyre, WrapErr}; +use color_eyre::Result; use log::debug; use regex::Regex; use std::fmt; @@ -89,7 +90,7 @@ impl fmt::Display for GitUrl { }; let host = match &self.host { - Some(host) => format!("{}", host), + Some(host) => host.to_string(), None => format!(""), }; @@ -106,7 +107,7 @@ impl fmt::Display for GitUrl { format!(":{}", &self.path) } } - _ => format!("{}", &self.path), + _ => (&self.path).to_string(), }; let git_url_str = format!("{}{}{}{}{}", scheme_prefix, auth_info, host, port, path); @@ -147,11 +148,12 @@ impl GitUrl { /// Returns a `Result` after normalizing and parsing `url` for metadata pub fn parse(url: &str) -> Result { // Normalize the url so we can use Url crate to process ssh urls - let normalized = normalize_url(url).expect("Url normalization failed"); + let normalized = normalize_url(url) + .with_context(|| "Url normalization into url::Url failed".to_string())?; // Some pre-processing for paths let scheme = Scheme::from_str(normalized.scheme()) - .expect(&format!("Scheme unsupported: {:?}", normalized.scheme())); + .with_context(|| format!("Scheme unsupported: {:?}", normalized.scheme()))?; // Normalized ssh urls can always have their first '/' removed let urlpath = match &scheme { @@ -176,7 +178,7 @@ impl GitUrl { // name = reponame // // organizations are going to be supported on a per-host basis - let splitpath = &urlpath.rsplit_terminator("/").collect::>(); + let splitpath = &urlpath.rsplit_terminator('/').collect::>(); debug!("rsplit results for metadata: {:?}", splitpath); let name = splitpath[0].trim_end_matches(".git").to_string(); @@ -191,8 +193,7 @@ impl GitUrl { let hosts_w_organization_in_path = vec!["dev.azure.com", "ssh.dev.azure.com"]; //vec!["dev.azure.com", "ssh.dev.azure.com", "visualstudio.com"]; - match hosts_w_organization_in_path.contains(&normalized.clone().host_str().unwrap()) - { + match hosts_w_organization_in_path.contains(&normalized.host_str().unwrap()) { true => { debug!("Found a git provider with an org"); @@ -202,34 +203,34 @@ impl GitUrl { // Example: "git@ssh.dev.azure.com:v3/CompanyName/ProjectName/RepoName", Scheme::Ssh => { // Organization - fullname.push(splitpath[2].clone()); + fullname.push(splitpath[2]); // Project/Owner name - fullname.push(splitpath[1].clone()); + fullname.push(splitpath[1]); // Repo name - fullname.push(splitpath[0].clone()); + fullname.push(splitpath[0]); ( Some(splitpath[1].to_string()), Some(splitpath[2].to_string()), - fullname.join("/").to_string(), + fullname.join("/"), ) } // Example: "https://CompanyName@dev.azure.com/CompanyName/ProjectName/_git/RepoName", Scheme::Https => { // Organization - fullname.push(splitpath[3].clone()); + fullname.push(splitpath[3]); // Project/Owner name - fullname.push(splitpath[2].clone()); + fullname.push(splitpath[2]); // Repo name - fullname.push(splitpath[0].clone()); + fullname.push(splitpath[0]); ( Some(splitpath[2].to_string()), Some(splitpath[3].to_string()), - fullname.join("/").to_string(), + fullname.join("/"), ) } - _ => panic!("Scheme not supported for host"), + _ => return Err(eyre!("Scheme not supported for host")), } } false => { @@ -241,54 +242,45 @@ impl GitUrl { ( Some(splitpath[1].to_string()), None::, - fullname.join("/").to_string(), + fullname.join("/"), ) } } } }; - let final_scheme = Scheme::from_str(normalized.scheme()).expect("Scheme unsupported"); - - let final_host = match final_scheme { + let final_host = match scheme { Scheme::File => None, - _ => match normalized.host_str() { - Some(h) => Some(h.to_string()), - None => None, - }, + _ => normalized.host_str().map(|h| h.to_string()), }; - let final_path = match final_scheme { + let final_path = match scheme { Scheme::File => { if let Some(host) = normalized.host_str() { format!("{}{}", host, urlpath) } else { urlpath } - }, + } _ => urlpath, }; Ok(GitUrl { host: final_host, - name: name, - owner: owner, - organization: organization, - fullname: fullname, - scheme: final_scheme, + name, + owner, + organization, + fullname, + scheme, user: match normalized.username().to_string().len() { 0 => None, _ => Some(normalized.username().to_string()), }, - token: match normalized.password() { - Some(p) => Some(p.to_string()), - None => None, - }, + token: normalized.password().map(|p| p.to_string()), port: normalized.port(), path: final_path, git_suffix: *git_suffix_check, scheme_prefix: url.contains("://"), - ..Default::default() }) } } @@ -300,7 +292,7 @@ impl GitUrl { /// /// Supports absolute and relative paths fn normalize_ssh_url(url: &str) -> Result { - let u = url.split(":").collect::>(); + let u = url.split(':').collect::>(); match u.len() { 2 => { @@ -311,9 +303,7 @@ fn normalize_ssh_url(url: &str) -> Result { debug!("Normalizing ssh url with ports: {:?}", u); normalize_url(&format!("ssh://{}:{}/{}", u[0], u[1], u[2])) } - _default => { - panic!("SSH normalization pattern not covered for: {:?}", u); - } + _default => Err(eyre!("SSH normalization pattern not covered for: {:?}", u)), } } @@ -325,10 +315,8 @@ fn normalize_file_path(filepath: &str) -> Result { match fp { Ok(path) => Ok(path), - Err(_e) => { - Ok(normalize_url(&format!("file://{}", filepath)) - .expect("file:// normalization failed")) - } + Err(_e) => Ok(normalize_url(&format!("file://{}", filepath)) + .with_context(|| "file:// normalization failed".to_string())?), } } @@ -338,10 +326,16 @@ fn normalize_file_path(filepath: &str) -> Result { pub fn normalize_url(url: &str) -> Result { debug!("Processing: {:?}", &url); + // Error if there are null bytes within the url + // https://github.com/tjtelan/git-url-parse-rs/issues/16 + if url.contains('\0') { + return Err(eyre!("Found null bytes within input url before parsing")); + } + // We're going to remove any trailing slash before running through Url::parse - let trim_url = url.trim_end_matches("/"); + let trim_url = url.trim_end_matches('/'); - let url_parse = Url::parse(&trim_url); + let url_parse = Url::parse(trim_url); Ok(match url_parse { Ok(u) => { @@ -350,7 +344,9 @@ pub fn normalize_url(url: &str) -> Result { Err(_e) => { // Catch case when an ssh url is given w/o a user debug!("Scheme parse fail. Assuming a userless ssh url"); - normalize_ssh_url(trim_url)? + normalize_ssh_url(trim_url).with_context(|| { + "No url scheme was found, then failed to normalize as ssh url.".to_string() + })? } } } @@ -360,16 +356,20 @@ pub fn normalize_url(url: &str) -> Result { // Assuming we have found Scheme::Ssh if we can find an "@" before ":" // Otherwise we have Scheme::File - let re = Regex::new(r"^\S+(@)\S+(:).*$")?; + let re = Regex::new(r"^\S+(@)\S+(:).*$").with_context(|| { + "Failed to build ssh git url regex for testing against url".to_string() + })?; - match re.is_match(&trim_url) { + match re.is_match(trim_url) { true => { debug!("Scheme::SSH match for normalization"); - normalize_ssh_url(trim_url)? + normalize_ssh_url(trim_url) + .with_context(|| "Failed to normalize as ssh url".to_string())? } false => { debug!("Scheme::File match for normalization"); - normalize_file_path(&format!("{}", trim_url))? + normalize_file_path(trim_url) + .with_context(|| "Failed to normalize as file url".to_string())? } } } diff --git a/tests/normalize.rs b/tests/normalize.rs index 1c55414..8b192b5 100644 --- a/tests/normalize.rs +++ b/tests/normalize.rs @@ -137,3 +137,21 @@ fn multi_git_ssh() { "git+ssh://host.tld/user/project-name.git" ); } + +// From https://github.com/tjtelan/git-url-parse-rs/issues/16 +#[test] +fn null_in_input1() { + let test_url = "////////ws///////////*,\u{0}\u{0}^\u{0}\u{0}\u{0}\u{0}@2\u{1}\u{0}\u{1d})\u{0}\u{0}\u{0}:\u{0}\u{0}\u{0}"; + let normalized = normalize_url(test_url); + + assert!(normalized.is_err()); +} + +// From https://github.com/tjtelan/git-url-parse-rs/issues/16 +#[test] +fn null_in_input2() { + let test_url = "?\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{1f}s\u{3}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{5}\u{1}@\u{0}\u{0}\u{4}!e\u{0}\u{0}2\u{1c}^3106://?