rust-netrc-0.1.2/.cargo_vcs_info.json0000644000000001360000000000100131420ustar { "git": { "sha1": "e5f96cc9cc78931b949b48c0758f15da331e9761" }, "path_in_vcs": "" }rust-netrc-0.1.2/.gitignore000064400000000000000000000006721046102023000137270ustar 00000000000000# Generated by Cargo # will have compiled files and executables debug/ target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb # Added by cargo /target rust-netrc-0.1.2/Cargo.toml0000644000000021460000000000100111430ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "rust-netrc" version = "0.1.2" authors = ["Hakim Taklanti "] exclude = ["/.github"] description = "netrc support for Rust with support for reqwest" documentation = "https://docs.rs/rust-netrc" readme = "README.md" keywords = [ "netrc", "reqwest", "http", "client", ] categories = ["network-programming"] license = "MIT" repository = "https://github.com/gribouille/netrc" [lib] name = "netrc" path = "src/lib.rs" [dependencies.shellexpand] version = "3.1.0" features = [ "base-0", "tilde", "path", ] default-features = false [dependencies.thiserror] version = "1.0.56" rust-netrc-0.1.2/Cargo.toml.orig000064400000000000000000000012121046102023000146150ustar 00000000000000[package] name = "rust-netrc" version = "0.1.2" description = "netrc support for Rust with support for reqwest" keywords = ["netrc", "reqwest", "http", "client"] authors = ["Hakim Taklanti "] edition = "2021" categories = ["network-programming"] readme = "README.md" repository = "https://github.com/gribouille/netrc" documentation = "https://docs.rs/rust-netrc" license = "MIT" exclude = ["/.github"] [lib] name = "netrc" path = "src/lib.rs" [dependencies] shellexpand = { version = "3.1.0", features = ["base-0", "tilde", "path"], default-features = false } thiserror = "1.0.56" [workspace] members = ["reqwest-netrc"] rust-netrc-0.1.2/LICENSE000064400000000000000000000020531046102023000127370ustar 00000000000000MIT License Copyright (c) 2024 Gribouille Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. rust-netrc-0.1.2/README.md000064400000000000000000000051611046102023000132140ustar 00000000000000# netrc A [netrc](https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html) library for Rust, with support for [reqwest](https://crates.io/crates/reqwest) via [reqwest-middleware](https://crates.io/crates/reqwest-middleware). ## reqwest-netrc The `reqwest-netrc` crate is a middleware for [reqwest](https://crates.io/crates/reqwest) to support the `netrc` file. [![crates.io][crates-badge-2]][crate-url-2] [![Documentation][doc-badge-2]][doc-url-2] [![MIT licensed][mit-badge]][mit-url] [![CI][actions-badge-2]][actions-url-2] ### Usage To bring this crate into your repository, either add `reqwest-netrc` to your `Cargo.toml`, or run: ``` > cargo add reqwest-netrc ``` ### Example The common scenario is to have a `~/.netrc` file or the `NETRC` environement variable defined: ```rust use reqwest::Client; use reqwest_middleware::ClientBuilder; use reqwest_netrc::NetrcMiddleware; // ... let client = ClientBuilder::new(Client::builder().build().unwrap()) .with_init(NetrcMiddleware::new().unwrap()) .build(); let res = client.get("https://domain.io/api/hello").send().await; // ... ``` ## rust-netrc The `rust-netrc` crate is a parser for the `netrc` files. [![crates.io][crates-badge]][crate-url] [![Documentation][doc-badge]][doc-url] [![MIT licensed][mit-badge]][mit-url] [![CI][actions-badge]][actions-url] ### Usage To bring this crate into your repository, either add `rust-netrc` to your `Cargo.toml`, or run: ``` > cargo add rust-netrc ``` ### Example ```rust use netrc::Netrc; fn main() { let nrc = Netrc::new().unwrap(); for (host, auth) in nrc.hosts { println!("{host}: {auth:?}"); } } ``` ## Contributing Feedback and contributions are very welcome. ## License This project is licensed under [MIT](./LICENSE). [mit-badge]: https://img.shields.io/crates/l/rust-netrc.svg [mit-url]: ./LICENSE [crates-badge]: https://img.shields.io/crates/v/rust-netrc.svg [crate-url]: https://crates.io/crates/rust-netrc [doc-badge]: https://docs.rs/rust-netrc/badge.svg [doc-url]: https://docs.rs/rust-netrc [actions-badge]: https://github.com/gribouille/netrc/actions/workflows/rust-netrc.yml/badge.svg [actions-url]: https://github.com/gribouille/netrc/actions/workflows/rust-netrc.yml [crates-badge-2]: https://img.shields.io/crates/v/reqwest-netrc.svg [crate-url-2]: https://crates.io/crates/reqwest-netrc [doc-badge-2]: https://docs.rs/reqwest-netrc/badge.svg [doc-url-2]: https://docs.rs/reqwest-netrc [actions-badge-2]: https://github.com/gribouille/netrc/actions/workflows/reqwest-netrc.yml/badge.svg [actions-url-2]: https://github.com/gribouille/netrc/actions/workflows/reqwest-netrc.ymlrust-netrc-0.1.2/src/lex.rs000064400000000000000000000047301046102023000136630ustar 00000000000000use std::collections::VecDeque; use std::str::Chars; pub struct Lex<'a> { pub lineno: u32, pub instream: Chars<'a>, pub pushback: VecDeque, } impl<'a> Lex<'a> { pub fn new(content: &'a str) -> Self { Lex { lineno: 1, instream: content.chars(), pushback: VecDeque::new(), } } pub fn read_char(&mut self) -> Option { let ch = self.instream.next(); if ch == Some('\n') { self.lineno += 1; } ch } pub fn read_line(&mut self) -> String { let mut s = String::new(); for ch in &mut self.instream { if ch == '\n' { return s; } s.push(ch); } s } pub fn get_token(&mut self) -> String { let p = self.pushback.pop_front(); if let Some(x) = p { return x; } let mut token = String::new(); while let Some(ch) = self.read_char() { match ch { '\n' | '\t' | '\r' | ' ' => { continue; } '"' => { while let Some(ch) = self.read_char() { match ch { '"' => { return token; } '\\' => { token.push(self.read_char().unwrap_or(' ')); } _ => { token.push(ch); } } } } _ => { let c = if ch == '\\' { self.read_char().unwrap_or(' ') } else { ch }; token.push(c); while let Some(ch) = self.read_char() { let c = match ch { '\n' | '\t' | '\r' | ' ' => { return token; } '\\' => self.read_char().unwrap_or(' '), _ => ch, }; token.push(c); } } } } token } pub fn push_token(&mut self, token: &str) { self.pushback.push_back(token.to_owned()); } } rust-netrc-0.1.2/src/lib.rs000064400000000000000000000103271046102023000136400ustar 00000000000000/*! The `netrc` crate provides a parser for the netrc file. The `reqwest-netrc` crate adds the support of netrc to the `reqwest` crate via the `reqwest-middleware` wrapper. # Setup ```text $ crago add rust-netrc ``` # Example ```no_run use netrc::Netrc; // ... let nrc = Netrc::new().unwrap(); // ... println!( "login = {}\naccount = {}\npassword = {}", nrc.hosts["my.host"].login, nrc.hosts["my.host"].account, nrc.hosts["my.host"].password, ); ``` */ pub use netrc::{Authenticator, Netrc}; use std::fs; use std::io; use std::io::ErrorKind; #[cfg(windows)] use std::iter::repeat; use std::path::{Path, PathBuf}; use std::result; mod lex; mod netrc; pub type Result = result::Result; /// An error that can occur when processing a Netrc file. #[derive(thiserror::Error, Debug)] pub enum Error { /// Wrap `std::io::Error` when we try to open the netrc file. #[error("I/O error: {0}")] Io(#[from] std::io::Error), /// Parsing error. #[error("{parser} in the file '{filename}'")] Parsing { parser: netrc::ParsingError, filename: String, }, } impl Netrc { /// Create a new `Netrc` object. /// /// Look up the `NETRC` environment variable if it is defined else that the /// default `~/.netrc` file. pub fn new() -> Result { Self::get_file() .ok_or(Error::Io(io::Error::new( ErrorKind::NotFound, "no netrc file found", ))) .and_then(|f| Netrc::from_file(f.as_path())) } /// Create a new `Netrc` object from a file. pub fn from_file(file: &Path) -> Result { String::from_utf8_lossy(&fs::read(file)?) .parse() .map_err(|e| Error::Parsing { parser: e, filename: file.display().to_string(), }) } /// Search a netrc file. /// /// Look up the `NETRC` environment variable if it is defined else use the .netrc (or _netrc /// file on windows) in the user's home directory. pub fn get_file() -> Option { let env_var = std::env::var("NETRC") .map(PathBuf::from) .map(|f| shellexpand::path::tilde(&f).into_owned()); #[cfg(windows)] let default = std::env::var("USERPROFILE") .into_iter() .flat_map(|home| repeat(home).zip([".netrc", "_netrc"])) .map(|(home, file)| PathBuf::from(home).join(file)); #[cfg(not(windows))] let default = std::env::var("HOME").map(|home| PathBuf::from(home).join(".netrc")); env_var.into_iter().chain(default).find(|f| f.exists()) } } #[cfg(test)] mod tests { use super::*; const CONTENT: &str = "\ machine cocolog-nifty.com login jmarten0 password cC2&yt7OT machine wired.com login mstanlack1 password gH4={wx=>VixU machine joomla.org login mbutterley2 password hY5>yKqU&$vq&0 "; fn create_netrc_file() -> PathBuf { let dest = std::env::temp_dir().join("mynetrc"); if !dest.exists() { std::fs::write(&dest, CONTENT).unwrap(); } dest } fn check_nrc(nrc: &Netrc) { assert_eq!(nrc.hosts.len(), 3); assert_eq!( nrc.hosts["cocolog-nifty.com"], Authenticator::new("jmarten0", "", "cC2&yt7OT") ); assert_eq!( nrc.hosts["wired.com"], Authenticator::new("mstanlack1", "", "gH4={wx=>VixU") ); assert_eq!( nrc.hosts["joomla.org"], Authenticator::new("mbutterley2", "", "hY5>yKqU&$vq&0") ); } #[test] fn test_new_env() { let fi = create_netrc_file(); std::env::set_var("NETRC", fi); let nrc = Netrc::new().unwrap(); check_nrc(&nrc); } #[test] fn test_new_default() {} #[test] fn test_from_file_failed() { assert_eq!( Netrc::from_file(Path::new("/netrc/file/not/exists/on/no/netrc")) .unwrap_err() .to_string(), "I/O error: No such file or directory (os error 2)" ); } #[test] fn test_from_file() { let fi = create_netrc_file(); let nrc = Netrc::from_file(fi.as_path()).unwrap(); check_nrc(&nrc); } } rust-netrc-0.1.2/src/netrc.rs000064400000000000000000000424151046102023000142100ustar 00000000000000//! This parser and the tests are a translation of the official Python netrc library. use crate::lex::Lex; use std::collections::HashMap; #[derive(Debug)] pub struct ParsingError { lineno: u32, message: String, } impl std::fmt::Display for ParsingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "parsing error: {} (line {})", self.message, self.lineno) } } /// Authenticators for host. #[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct Authenticator { /// Identify a user on the remote machine. pub login: String, /// Supply an additional account password. pub account: String, /// Supply a password pub password: String, } impl Authenticator { #[allow(dead_code)] pub fn new(login: &str, account: &str, password: &str) -> Self { Authenticator { login: login.to_owned(), account: account.to_owned(), password: password.to_owned(), } } } /// Represents the netrc file. #[derive(Debug, Default)] pub struct Netrc { /// Dictionary mapping host names to the authentificators. pub hosts: HashMap, /// Dictionary mapping macro names to string lists. pub macros: HashMap>, } impl std::fmt::Display for Netrc { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut rep = String::new(); for (host, attrs) in self.hosts.iter() { rep.push_str(&format!("machine {}\n\tlogin {}\n", host, attrs.login)); if !attrs.account.is_empty() { rep.push_str(&format!("\taccount {}\n", attrs.account)); } rep.push_str(&format!("\tpassword {}\n", attrs.password)); } for (macro_, lines) in self.macros.iter() { rep.push_str(&format!("macdef {}\n", macro_)); for line in lines.iter() { rep.push_str(&format!("{}\n", line)); } } write!(f, "{}", rep) } } impl std::str::FromStr for Netrc { type Err = ParsingError; fn from_str(s: &str) -> Result { let mut res = Netrc::default(); let mut lexer = Lex::new(s); loop { let saved_lineno = lexer.lineno; let tt = lexer.get_token(); if tt.is_empty() { break; } if tt.chars().nth(0) == Some('#') { if lexer.lineno == saved_lineno && tt.len() == 1 { lexer.read_line(); } continue; } #[allow(clippy::needless_late_init)] let entryname; match tt.as_str() { "" => { break; } "machine" => { entryname = lexer.get_token(); } "default" => { entryname = String::from("default"); } "macdef" => { entryname = lexer.get_token(); let mut v = Vec::new(); loop { let line = lexer.read_line(); if line.trim().is_empty() { break; } v.push(line.trim().to_owned()); } res.macros.insert(entryname, v); continue; } _ => { return Err(ParsingError { lineno: lexer.lineno, message: format!("bad toplevel token '{}'", tt), }); } }; if entryname.is_empty() { return Err(ParsingError { lineno: lexer.lineno, message: format!("missing '{}' name", tt), }); } let mut auth = Authenticator::default(); loop { let prev_lineno = lexer.lineno; let tt = lexer.get_token(); if tt.starts_with('#') { if lexer.lineno == prev_lineno { lexer.read_line(); } continue; } match tt.as_str() { "" | "machine" | "default" | "macdef" => { res.hosts.insert(entryname, auth); lexer.push_token(&tt); break; } "login" | "user" => { auth.login = lexer.get_token(); } "account" => { auth.account = lexer.get_token(); } "password" => { auth.password = lexer.get_token(); } _ => { return Err(ParsingError { lineno: lexer.lineno, message: format!("bad follower token '{}'", tt), }); } }; } } Ok(res) } } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; #[test] fn test_toplevel_non_ordered_tokens() { let nrc = Netrc::from_str( "\ machine host.domain.com password pass1 login log1 account acct1 default login log2 password pass2 account acct2 ", ) .unwrap(); assert_eq!( nrc.hosts["host.domain.com"], Authenticator::new("log1", "acct1", "pass1") ); assert_eq!( nrc.hosts["default"], Authenticator::new("log2", "acct2", "pass2") ); } #[test] fn test_toplevel_tokens() { let nrc = Netrc::from_str( "\ machine host.domain.com login log1 password pass1 account acct1 default login log2 password pass2 account acct2 ", ) .unwrap(); assert_eq!( nrc.hosts["host.domain.com"], Authenticator::new("log1", "acct1", "pass1") ); assert_eq!( nrc.hosts["default"], Authenticator::new("log2", "acct2", "pass2") ); } #[test] fn test_macros() { let nrc = Netrc::from_str( "\ macdef macro1 line1 line2 macdef macro2 line3 line4 ", ) .unwrap(); assert_eq!(nrc.macros["macro1"], vec!["line1", "line2"]); assert_eq!(nrc.macros["macro2"], vec!["line3", "line4"]); } #[test] fn test_optional_tokens_machine() { let data = vec![ "machine host.domain.com", "machine host.domain.com login", "machine host.domain.com account", "machine host.domain.com password", "machine host.domain.com login \"\" account", "machine host.domain.com login \"\" password", "machine host.domain.com account \"\" password", ]; for item in data { let nrc = Netrc::from_str(item).unwrap(); assert_eq!(nrc.hosts["host.domain.com"], Authenticator::new("", "", "")); } } #[test] fn test_optional_tokens_default() { let data = vec![ "default", "default login", "default account", "default password", "default login \"\" account", "default login \"\" password", "default account \"\" password", ]; for item in data { let nrc = Netrc::from_str(item).unwrap(); assert_eq!(nrc.hosts["default"], Authenticator::new("", "", "")); } } #[test] fn test_invalid_tokens() { let data = vec![ ( "invalid host.domain.com", "parsing error: bad toplevel token 'invalid' (line 1)", ), ( "machine host.domain.com invalid", "parsing error: bad follower token 'invalid' (line 1)", ), ( "machine host.domain.com login log password pass account acct invalid", "parsing error: bad follower token 'invalid' (line 1)", ), ( "default host.domain.com invalid", "parsing error: bad follower token 'host.domain.com' (line 1)", ), ( "default host.domain.com login log password pass account acct invalid", "parsing error: bad follower token 'host.domain.com' (line 1)", ), ]; for (item, msg) in data { let nrc = Netrc::from_str(item); assert_eq!(nrc.unwrap_err().to_string(), msg); } } fn test_token_x(data: &str, token: &str, value: &str) { let nrc = Netrc::from_str(data).unwrap(); match token { "login" => { assert_eq!( nrc.hosts["host.domain.com"], Authenticator::new(value, "acct", "pass") ); } "account" => { assert_eq!( nrc.hosts["host.domain.com"], Authenticator::new("log", value, "pass") ); } "password" => { assert_eq!( nrc.hosts["host.domain.com"], Authenticator::new("log", "acct", value) ); } _ => {} }; } #[test] fn test_token_value_quotes() { test_token_x( "\ machine host.domain.com login \"log\" password pass account acct ", "login", "log", ); test_token_x( "\ machine host.domain.com login log password pass account \"acct\" ", "account", "acct", ); test_token_x( "\ machine host.domain.com login log password \"pass\" account acct ", "password", "pass", ); } #[test] fn test_token_value_escape() { test_token_x( r#"machine host.domain.com login \"log password pass account acct"#, "login", "\"log", ); test_token_x( "\ machine host.domain.com login \"\\\"log\" password pass account acct ", "login", "\"log", ); test_token_x( "\ machine host.domain.com login log password pass account \\\"acct ", "account", "\"acct", ); test_token_x( "\ machine host.domain.com login log password pass account \"\\\"acct\" ", "account", "\"acct", ); test_token_x( "\ machine host.domain.com login log password \\\"pass account acct ", "password", "\"pass", ); test_token_x( "\ machine host.domain.com login log password \"\\\"pass\" account acct ", "password", "\"pass", ); } #[test] fn test_token_value_whitespace() { test_token_x( r#"machine host.domain.com login "lo g" password pass account acct"#, "login", "lo g", ); test_token_x( r#"machine host.domain.com login log password "pas s" account acct"#, "password", "pas s", ); test_token_x( r#"machine host.domain.com login log password pass account "acc t""#, "account", "acc t", ); } #[test] fn test_token_value_non_ascii() { test_token_x( r#"machine host.domain.com login ¡¢ password pass account acct"#, "login", "¡¢", ); test_token_x( r#"machine host.domain.com login log password pass account ¡¢"#, "account", "¡¢", ); test_token_x( r#"machine host.domain.com login log password ¡¢ account acct"#, "password", "¡¢", ); } #[test] fn test_token_value_leading_hash() { test_token_x( r#"machine host.domain.com login #log password pass account acct"#, "login", "#log", ); test_token_x( r#"machine host.domain.com login log password pass account #acct"#, "account", "#acct", ); test_token_x( r#"machine host.domain.com login log password #pass account acct"#, "password", "#pass", ); } #[test] fn test_token_value_trailing_hash() { test_token_x( r#"machine host.domain.com login log# password pass account acct"#, "login", "log#", ); test_token_x( r#"machine host.domain.com login log password pass account acct#"#, "account", "acct#", ); test_token_x( r#"machine host.domain.com login log password pass# account acct"#, "password", "pass#", ); } #[test] fn test_token_value_internal_hash() { test_token_x( r#"machine host.domain.com login lo#g password pass account acct"#, "login", "lo#g", ); test_token_x( r#"machine host.domain.com login log password pass account ac#ct"#, "account", "ac#ct", ); test_token_x( r#"machine host.domain.com login log password pa#ss account acct"#, "password", "pa#ss", ); } fn test_comment(data: &str) { let nrc = Netrc::from_str(data).unwrap(); assert_eq!( nrc.hosts["foo.domain.com"], Authenticator::new("bar", "", "pass") ); assert_eq!( nrc.hosts["bar.domain.com"], Authenticator::new("foo", "", "pass") ); } #[test] fn test_comment_before_machine_line() { test_comment( r#"# comment machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass "#, ); } #[test] fn test_comment_before_machine_line_no_space() { test_comment( r#"#comment machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass "#, ); } #[test] fn test_comment_before_machine_line_hash_only() { test_comment( r#"# machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass "#, ); } #[test] fn test_comment_after_machine_line() { test_comment( r#"machine foo.domain.com login bar password pass # comment machine bar.domain.com login foo password pass "#, ); test_comment( r#"machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass # comment "#, ); } #[test] fn test_comment_after_machine_line_no_space() { test_comment( r#"machine foo.domain.com login bar password pass #comment machine bar.domain.com login foo password pass "#, ); test_comment( r#"machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass #comment "#, ); } #[test] fn test_comment_after_machine_line_hash_only() { test_comment( r#"machine foo.domain.com login bar password pass # machine bar.domain.com login foo password pass "#, ); test_comment( r#"machine foo.domain.com login bar password pass machine bar.domain.com login foo password pass # "#, ); } #[test] fn test_comment_at_end_of_machine_line() { test_comment( r#"machine foo.domain.com login bar password pass # comment machine bar.domain.com login foo password pass "#, ); } #[test] fn test_comment_at_end_of_machine_line_no_space() { test_comment( r#"machine foo.domain.com login bar password pass #comment machine bar.domain.com login foo password pass "#, ); } #[test] fn test_comment_at_end_of_machine_line_pass_has_hash() { let nrc = Netrc::from_str( r#"machine foo.domain.com login bar password #pass #comment machine bar.domain.com login foo password pass "#, ) .unwrap(); assert_eq!( nrc.hosts["foo.domain.com"], Authenticator::new("bar", "", "#pass") ); assert_eq!( nrc.hosts["bar.domain.com"], Authenticator::new("foo", "", "pass") ); } }