diff options
Diffstat (limited to 'src/rust')
30 files changed, 2881 insertions, 214 deletions
diff --git a/src/rust/.cargo/config.in b/src/rust/.cargo/config.in index 414b253a57..301e7fdbe7 100644 --- a/src/rust/.cargo/config.in +++ b/src/rust/.cargo/config.in @@ -5,4 +5,4 @@ @RUST_DL@ replace-with = 'vendored-sources' @RUST_DL@ [source.vendored-sources] -@RUST_DL@ directory = '@RUST_DEPENDENCIES@' +@RUST_DL@ directory = '@TOR_RUST_DEPENDENCIES@' diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index e208e14320..4f918c0221 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -1,14 +1,55 @@ [[package]] +name = "external" +version = "0.0.1" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "libc" -version = "0.2.22" +version = "0.2.39" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "protover" +version = "0.0.1" +dependencies = [ + "external 0.0.1", + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "smartlist 0.0.1", + "tor_allocate 0.0.1", + "tor_util 0.0.1", +] + +[[package]] +name = "smartlist" +version = "0.0.1" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tor_allocate" +version = "0.0.1" +dependencies = [ + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tor_rust" +version = "0.1.0" +dependencies = [ + "protover 0.0.1", + "tor_util 0.0.1", +] + +[[package]] name = "tor_util" version = "0.0.1" dependencies = [ - "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)", + "tor_allocate 0.0.1", ] [metadata] -"checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502" +"checksum libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)" = "f54263ad99207254cf58b5f701ecb432c717445ea2ee8af387334bdd1a03fdff" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index fc4377e8b4..953c9b96b7 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["tor_util"] +members = ["tor_util", "protover", "smartlist", "external", "tor_allocate", "tor_rust"] [profile.release] debug = true diff --git a/src/rust/external/Cargo.toml b/src/rust/external/Cargo.toml new file mode 100644 index 0000000000..b5957b1079 --- /dev/null +++ b/src/rust/external/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "external" + +[dependencies] +libc = "=0.2.39" + +[lib] +name = "external" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/external/external.rs b/src/rust/external/external.rs new file mode 100644 index 0000000000..b9e17f021d --- /dev/null +++ b/src/rust/external/external.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +use libc::{c_char, c_int}; +use std::ffi::CString; + +extern "C" { + fn tor_version_as_new_as( + platform: *const c_char, + cutoff: *const c_char, + ) -> c_int; +} + +/// Wrap calls to tor_version_as_new_as, defined in src/or/routerparse.c +pub fn c_tor_version_as_new_as(platform: &str, cutoff: &str) -> bool { + // CHK: These functions should log a warning if an error occurs. This + // can be added when integration with tor's logger is added to rust + let c_platform = match CString::new(platform) { + Ok(n) => n, + Err(_) => return false, + }; + + let c_cutoff = match CString::new(cutoff) { + Ok(n) => n, + Err(_) => return false, + }; + + let result: c_int = unsafe { + tor_version_as_new_as(c_platform.as_ptr(), c_cutoff.as_ptr()) + }; + + result == 1 +} diff --git a/src/rust/external/lib.rs b/src/rust/external/lib.rs new file mode 100644 index 0000000000..0af0d6452d --- /dev/null +++ b/src/rust/external/lib.rs @@ -0,0 +1,14 @@ +//! Copyright (c) 2016-2017, The Tor Project, Inc. */ +//! See LICENSE for licensing information */ + +//! Interface for external calls to tor C ABI +//! +//! The purpose of this module is to provide a clean interface for when Rust +//! modules need to interact with functionality in tor C code rather than each +//! module implementing this functionality repeatedly. + +extern crate libc; + +mod external; + +pub use external::*; diff --git a/src/rust/include.am b/src/rust/include.am index 20afc6c4db..7a0181e373 100644 --- a/src/rust/include.am +++ b/src/rust/include.am @@ -1,6 +1,29 @@ -include src/rust/tor_util/include.am +include src/rust/tor_rust/include.am EXTRA_DIST +=\ src/rust/Cargo.toml \ src/rust/Cargo.lock \ - src/rust/.cargo/config.in + src/rust/.cargo/config.in \ + src/rust/external/Cargo.toml \ + src/rust/external/external.rs \ + src/rust/external/lib.rs \ + src/rust/protover/Cargo.toml \ + src/rust/protover/errors.rs \ + src/rust/protover/protoset.rs \ + src/rust/protover/ffi.rs \ + src/rust/protover/lib.rs \ + src/rust/protover/protover.rs \ + src/rust/protover/tests/protover.rs \ + src/rust/smartlist/Cargo.toml \ + src/rust/smartlist/lib.rs \ + src/rust/smartlist/smartlist.rs \ + src/rust/tor_allocate/Cargo.toml \ + src/rust/tor_allocate/lib.rs \ + src/rust/tor_allocate/tor_allocate.rs \ + src/rust/tor_rust/Cargo.toml \ + src/rust/tor_rust/include.am \ + src/rust/tor_rust/lib.rs \ + src/rust/tor_util/Cargo.toml \ + src/rust/tor_util/ffi.rs \ + src/rust/tor_util/lib.rs \ + src/rust/tor_util/strings.rs diff --git a/src/rust/protover/Cargo.toml b/src/rust/protover/Cargo.toml new file mode 100644 index 0000000000..86301b8787 --- /dev/null +++ b/src/rust/protover/Cargo.toml @@ -0,0 +1,25 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "protover" + +[dependencies] +libc = "=0.2.39" + +[dependencies.smartlist] +path = "../smartlist" + +[dependencies.external] +path = "../external" + +[dependencies.tor_util] +path = "../tor_util" + +[dependencies.tor_allocate] +path = "../tor_allocate" + +[lib] +name = "protover" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/protover/errors.rs b/src/rust/protover/errors.rs new file mode 100644 index 0000000000..56473d12e6 --- /dev/null +++ b/src/rust/protover/errors.rs @@ -0,0 +1,43 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Various errors which may occur during protocol version parsing. + +use std::fmt; +use std::fmt::Display; + +/// All errors which may occur during protover parsing routines. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[allow(missing_docs)] // See Display impl for error descriptions +pub enum ProtoverError { + Overlap, + LowGreaterThanHigh, + Unparseable, + ExceedsMax, + ExceedsExpansionLimit, + UnknownProtocol, + ExceedsNameLimit, +} + +/// Descriptive error messages for `ProtoverError` variants. +impl Display for ProtoverError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ProtoverError::Overlap + => write!(f, "Two or more (low, high) protover ranges would overlap once expanded."), + ProtoverError::LowGreaterThanHigh + => write!(f, "The low in a (low, high) protover range was greater than high."), + ProtoverError::Unparseable + => write!(f, "The protover string was unparseable."), + ProtoverError::ExceedsMax + => write!(f, "The high in a (low, high) protover range exceeds u32::MAX."), + ProtoverError::ExceedsExpansionLimit + => write!(f, "The protover string would exceed the maximum expansion limit."), + ProtoverError::UnknownProtocol + => write!(f, "A protocol in the protover string we attempted to parse is unknown."), + ProtoverError::ExceedsNameLimit + => write!(f, "An unrecognised protocol name was too long."), + } + } +} diff --git a/src/rust/protover/ffi.rs b/src/rust/protover/ffi.rs new file mode 100644 index 0000000000..9656e8c318 --- /dev/null +++ b/src/rust/protover/ffi.rs @@ -0,0 +1,296 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! FFI functions, only to be called from C. +//! +//! Equivalent C versions of this api are in `src/or/protover.c` + +use libc::{c_char, c_int, uint32_t}; +use std::ffi::CStr; +use std::ffi::CString; + +use smartlist::*; +use tor_allocate::allocate_and_copy_string; +use tor_util::strings::byte_slice_is_c_like; +use tor_util::strings::empty_static_cstr; + + +use errors::ProtoverError; +use protover::*; + +/// Translate C enums to Rust Proto enums, using the integer value of the C +/// enum to map to its associated Rust enum. +/// +/// C_RUST_COUPLED: src/or/protover.h `protocol_type_t` +fn translate_to_rust(c_proto: uint32_t) -> Result<Protocol, ProtoverError> { + match c_proto { + 0 => Ok(Protocol::Link), + 1 => Ok(Protocol::LinkAuth), + 2 => Ok(Protocol::Relay), + 3 => Ok(Protocol::DirCache), + 4 => Ok(Protocol::HSDir), + 5 => Ok(Protocol::HSIntro), + 6 => Ok(Protocol::HSRend), + 7 => Ok(Protocol::Desc), + 8 => Ok(Protocol::Microdesc), + 9 => Ok(Protocol::Cons), + _ => Err(ProtoverError::UnknownProtocol), + } +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::all_supported +#[no_mangle] +pub extern "C" fn protover_all_supported( + c_relay_version: *const c_char, + missing_out: *mut *mut c_char, +) -> c_int { + + if c_relay_version.is_null() { + return 1; + } + + // Require an unsafe block to read the version from a C string. The pointer + // is checked above to ensure it is not null. + let c_str: &CStr = unsafe { CStr::from_ptr(c_relay_version) }; + + let relay_version = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1, + }; + + let relay_proto_entry: UnvalidatedProtoEntry = + match UnvalidatedProtoEntry::from_str_any_len(relay_version) { + Ok(n) => n, + Err(_) => return 1, + }; + let maybe_unsupported: Option<UnvalidatedProtoEntry> = relay_proto_entry.all_supported(); + + if maybe_unsupported.is_some() { + let unsupported: UnvalidatedProtoEntry = maybe_unsupported.unwrap(); + let c_unsupported: CString = match CString::new(unsupported.to_string()) { + Ok(n) => n, + Err(_) => return 1, + }; + + let ptr = c_unsupported.into_raw(); + unsafe { *missing_out = ptr }; + + return 0; + } + + 1 +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::list_supports_protocol +#[no_mangle] +pub extern "C" fn protocol_list_supports_protocol( + c_protocol_list: *const c_char, + c_protocol: uint32_t, + version: uint32_t, +) -> c_int { + if c_protocol_list.is_null() { + return 1; + } + + // Require an unsafe block to read the version from a C string. The pointer + // is checked above to ensure it is not null. + let c_str: &CStr = unsafe { CStr::from_ptr(c_protocol_list) }; + + let protocol_list = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1, + }; + let proto_entry: UnvalidatedProtoEntry = match protocol_list.parse() { + Ok(n) => n, + Err(_) => return 0, + }; + let protocol: UnknownProtocol = match translate_to_rust(c_protocol) { + Ok(n) => n.into(), + Err(_) => return 0, + }; + match proto_entry.supports_protocol(&protocol, &version) { + false => return 0, + true => return 1, + } +} + +#[no_mangle] +pub extern "C" fn protover_contains_long_protocol_names_( + c_protocol_list: *const c_char +) -> c_int { + if c_protocol_list.is_null() { + return 1; + } + + // Require an unsafe block to read the version from a C string. The pointer + // is checked above to ensure it is not null. + let c_str: &CStr = unsafe { CStr::from_ptr(c_protocol_list) }; + + let protocol_list = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1 + }; + + let protocol_entry : Result<UnvalidatedProtoEntry,_> = + protocol_list.parse(); + + match protocol_entry { + Ok(_) => 0, + Err(_) => 1, + } +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::list_supports_protocol_or_later +#[no_mangle] +pub extern "C" fn protocol_list_supports_protocol_or_later( + c_protocol_list: *const c_char, + c_protocol: uint32_t, + version: uint32_t, +) -> c_int { + if c_protocol_list.is_null() { + return 1; + } + + // Require an unsafe block to read the version from a C string. The pointer + // is checked above to ensure it is not null. + let c_str: &CStr = unsafe { CStr::from_ptr(c_protocol_list) }; + + let protocol_list = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1, + }; + + let protocol = match translate_to_rust(c_protocol) { + Ok(n) => n, + Err(_) => return 0, + }; + + let proto_entry: UnvalidatedProtoEntry = match protocol_list.parse() { + Ok(n) => n, + Err(_) => return 1, + }; + + if proto_entry.supports_protocol_or_later(&protocol.into(), &version) { + return 1; + } + 0 +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::get_supported_protocols +#[no_mangle] +pub extern "C" fn protover_get_supported_protocols() -> *const c_char { + let supported: &'static CStr; + + // If we're going to pass it to C, there cannot be any intermediate NUL + // bytes. An assert is okay here, since changing the const byte slice + // in protover.rs to contain a NUL byte somewhere in the middle would be a + // programming error. + assert!(byte_slice_is_c_like(SUPPORTED_PROTOCOLS)); + + // It's okay to unwrap the result of this function because + // we can see that the bytes we're passing into it 1) are valid UTF-8, + // 2) have no intermediate NUL bytes, and 3) are terminated with a NUL + // byte. + supported = CStr::from_bytes_with_nul(SUPPORTED_PROTOCOLS).unwrap(); + + supported.as_ptr() +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::compute_vote +// +// Why is the threshold a signed integer? —isis +#[no_mangle] +pub extern "C" fn protover_compute_vote( + list: *const Stringlist, + threshold: c_int, + allow_long_proto_names: bool, +) -> *mut c_char { + + if list.is_null() { + let empty = String::new(); + return allocate_and_copy_string(&empty); + } + + // Dereference of raw pointer requires an unsafe block. The pointer is + // checked above to ensure it is not null. + let data: Vec<String> = unsafe { (*list).get_list() }; + let hold: usize = threshold as usize; + let mut proto_entries: Vec<UnvalidatedProtoEntry> = Vec::new(); + + for datum in data { + let entry: UnvalidatedProtoEntry = match allow_long_proto_names { + true => match UnvalidatedProtoEntry::from_str_any_len(datum.as_str()) { + Ok(n) => n, + Err(_) => continue}, + false => match datum.parse() { + Ok(n) => n, + Err(_) => continue}, + }; + proto_entries.push(entry); + } + let vote: UnvalidatedProtoEntry = ProtoverVote::compute(&proto_entries, &hold); + + allocate_and_copy_string(&vote.to_string()) +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::is_supported_here +#[no_mangle] +pub extern "C" fn protover_is_supported_here( + c_protocol: uint32_t, + version: uint32_t, +) -> c_int { + let protocol = match translate_to_rust(c_protocol) { + Ok(n) => n, + Err(_) => return 0, + }; + + let is_supported = is_supported_here(&protocol, &version); + + return if is_supported { 1 } else { 0 }; +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::compute_for_old_tor +#[no_mangle] +pub extern "C" fn protover_compute_for_old_tor(version: *const c_char) -> *const c_char { + let supported: &'static CStr; + let elder_protocols: &'static [u8]; + let empty: &'static CStr; + + empty = empty_static_cstr(); + + if version.is_null() { + return empty.as_ptr(); + } + + // Require an unsafe block to read the version from a C string. The pointer + // is checked above to ensure it is not null. + let c_str: &CStr = unsafe { CStr::from_ptr(version) }; + + let version = match c_str.to_str() { + Ok(n) => n, + Err(_) => return empty.as_ptr(), + }; + + elder_protocols = compute_for_old_tor_cstr(&version); + + // If we're going to pass it to C, there cannot be any intermediate NUL + // bytes. An assert is okay here, since changing the const byte slice + // in protover.rs to contain a NUL byte somewhere in the middle would be a + // programming error. + assert!(byte_slice_is_c_like(elder_protocols)); + + // It's okay to unwrap the result of this function because + // we can see that the bytes we're passing into it 1) are valid UTF-8, + // 2) have no intermediate NUL bytes, and 3) are terminated with a NUL + // byte. + supported = CStr::from_bytes_with_nul(elder_protocols).unwrap_or(empty); + + supported.as_ptr() +} diff --git a/src/rust/protover/lib.rs b/src/rust/protover/lib.rs new file mode 100644 index 0000000000..483260bca8 --- /dev/null +++ b/src/rust/protover/lib.rs @@ -0,0 +1,38 @@ +//! Copyright (c) 2016-2017, The Tor Project, Inc. */ +//! See LICENSE for licensing information */ + +//! Versioning information for different pieces of the Tor protocol. +//! +//! The below description is taken from src/rust/protover.c, which is currently +//! enabled by default. We are in the process of experimenting with Rust in +//! tor, and this protover module is implemented to help achieve this goal. +//! +//! Starting in version 0.2.9.3-alpha, Tor places separate version numbers on +//! each of the different components of its protocol. Relays use these numbers +//! to advertise what versions of the protocols they can support, and clients +//! use them to find what they can ask a given relay to do. Authorities vote +//! on the supported protocol versions for each relay, and also vote on the +//! which protocols you should have to support in order to be on the Tor +//! network. All Tor instances use these required/recommended protocol versions +//! to tell what level of support for recent protocols each relay has, and +//! to decide whether they should be running given their current protocols. +//! +//! The main advantage of these protocol versions numbers over using Tor +//! version numbers is that they allow different implementations of the Tor +//! protocols to develop independently, without having to claim compatibility +//! with specific versions of Tor. + +#[deny(missing_docs)] + +extern crate libc; +extern crate smartlist; +extern crate external; +extern crate tor_allocate; +extern crate tor_util; + +pub mod errors; +pub mod protoset; +mod protover; +pub mod ffi; + +pub use protover::*; diff --git a/src/rust/protover/protoset.rs b/src/rust/protover/protoset.rs new file mode 100644 index 0000000000..4afc50edf8 --- /dev/null +++ b/src/rust/protover/protoset.rs @@ -0,0 +1,634 @@ +// Copyright (c) 2018, The Tor Project, Inc. +// Copyright (c) 2018, isis agora lovecruft +// See LICENSE for licensing information + +//! Sets for lazily storing ordered, non-overlapping ranges of integers. + +use std::slice; +use std::str::FromStr; +use std::u32; + +use errors::ProtoverError; + +/// A single version number. +pub type Version = u32; + +/// A `ProtoSet` stores an ordered `Vec<T>` of `(low, high)` pairs of ranges of +/// non-overlapping protocol versions. +/// +/// # Examples +/// +/// ``` +/// use std::str::FromStr; +/// +/// use protover::errors::ProtoverError; +/// use protover::protoset::ProtoSet; +/// use protover::protoset::Version; +/// +/// # fn do_test() -> Result<ProtoSet, ProtoverError> { +/// let protoset: ProtoSet = ProtoSet::from_str("3-5,8")?; +/// +/// // We could also equivalently call: +/// let protoset: ProtoSet = "3-5,8".parse()?; +/// +/// assert!(protoset.contains(&4)); +/// assert!(!protoset.contains(&7)); +/// +/// let expanded: Vec<Version> = protoset.clone().into(); +/// +/// assert_eq!(&expanded[..], &[3, 4, 5, 8]); +/// +/// let contracted: String = protoset.clone().to_string(); +/// +/// assert_eq!(contracted, "3-5,8".to_string()); +/// # Ok(protoset) +/// # } +/// # fn main() { do_test(); } // wrap the test so we can use the ? operator +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct ProtoSet { + pub(crate) pairs: Vec<(Version, Version)>, +} + +impl Default for ProtoSet { + fn default() -> Self { + let pairs: Vec<(Version, Version)> = Vec::new(); + + ProtoSet{ pairs } + } +} + +impl<'a> ProtoSet { + /// Create a new `ProtoSet` from a slice of `(low, high)` pairs. + /// + /// # Inputs + /// + /// We do not assume the input pairs are deduplicated or ordered. + pub fn from_slice(low_high_pairs: &'a [(Version, Version)]) -> Result<Self, ProtoverError> { + let mut pairs: Vec<(Version, Version)> = Vec::with_capacity(low_high_pairs.len()); + + for &(low, high) in low_high_pairs { + pairs.push((low, high)); + } + // Sort the pairs without reallocation and remove all duplicate pairs. + pairs.sort_unstable(); + pairs.dedup(); + + ProtoSet{ pairs }.is_ok() + } +} + +/// Expand this `ProtoSet` to a `Vec` of all its `Version`s. +/// +/// # Examples +/// +/// ``` +/// use std::str::FromStr; +/// use protover::protoset::ProtoSet; +/// use protover::protoset::Version; +/// # use protover::errors::ProtoverError; +/// +/// # fn do_test() -> Result<Vec<Version>, ProtoverError> { +/// let protoset: ProtoSet = ProtoSet::from_str("3-5,21")?; +/// let versions: Vec<Version> = protoset.into(); +/// +/// assert_eq!(&versions[..], &[3, 4, 5, 21]); +/// # +/// # Ok(versions) +/// # } +/// # fn main() { do_test(); } // wrap the test so we can use the ? operator +/// ``` +impl Into<Vec<Version>> for ProtoSet { + fn into(self) -> Vec<Version> { + let mut versions: Vec<Version> = Vec::new(); + + for &(low, high) in self.iter() { + versions.extend(low..high + 1); + } + versions + } +} + +impl ProtoSet { + /// Get an iterator over the `(low, high)` `pairs` in this `ProtoSet`. + pub fn iter(&self) -> slice::Iter<(Version, Version)> { + self.pairs.iter() + } + + /// Expand this `ProtoSet` into a `Vec` of all its `Version`s. + /// + /// # Examples + /// + /// ``` + /// # use protover::errors::ProtoverError; + /// use protover::protoset::ProtoSet; + /// + /// # fn do_test() -> Result<bool, ProtoverError> { + /// let protoset: ProtoSet = "3-5,9".parse()?; + /// + /// assert_eq!(protoset.expand(), vec![3, 4, 5, 9]); + /// + /// let protoset: ProtoSet = "1,3,5-7".parse()?; + /// + /// assert_eq!(protoset.expand(), vec![1, 3, 5, 6, 7]); + /// # + /// # Ok(true) + /// # } + /// # fn main() { do_test(); } // wrap the test so we can use the ? operator + /// ``` + pub fn expand(self) -> Vec<Version> { + self.into() + } + + pub fn len(&self) -> usize { + let mut length: usize = 0; + + for &(low, high) in self.iter() { + length += (high as usize - low as usize) + 1; + } + + length + } + + /// Check that this `ProtoSet` is well-formed. + /// + /// This is automatically called in `ProtoSet::from_str()`. + /// + /// # Errors + /// + /// * `ProtoverError::LowGreaterThanHigh`: if its `pairs` were not + /// well-formed, i.e. a `low` in a `(low, high)` was higher than the + /// previous `high`, + /// * `ProtoverError::Overlap`: if one or more of the `pairs` are + /// overlapping, + /// * `ProtoverError::ExceedsMax`: if the number of versions when expanded + /// would exceed `MAX_PROTOCOLS_TO_EXPAND`, and + /// + /// # Returns + /// + /// A `Result` whose `Ok` is this `Protoset`, and whose `Err` is one of the + /// errors enumerated in the Errors section above. + fn is_ok(self) -> Result<ProtoSet, ProtoverError> { + let mut last_high: Version = 0; + + for &(low, high) in self.iter() { + if low == u32::MAX || high == u32::MAX { + return Err(ProtoverError::ExceedsMax); + } + if low < last_high { + return Err(ProtoverError::Overlap); + } else if low > high { + return Err(ProtoverError::LowGreaterThanHigh); + } + last_high = high; + } + + Ok(self) + } + + /// Determine if this `ProtoSet` contains no `Version`s. + /// + /// # Returns + /// + /// * `true` if this `ProtoSet`'s length is zero, and + /// * `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// use protover::protoset::ProtoSet; + /// + /// let protoset: ProtoSet = ProtoSet::default(); + /// + /// assert!(protoset.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.pairs.len() == 0 + } + + /// Determine if `version` is included within this `ProtoSet`. + /// + /// # Inputs + /// + /// * `version`: a `Version`. + /// + /// # Returns + /// + /// `true` if the `version` is contained within this set; `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use protover::errors::ProtoverError; + /// use protover::protoset::ProtoSet; + /// + /// # fn do_test() -> Result<ProtoSet, ProtoverError> { + /// let protoset: ProtoSet = ProtoSet::from_slice(&[(0, 5), (7, 9), (13, 14)])?; + /// + /// assert!(protoset.contains(&5)); + /// assert!(!protoset.contains(&10)); + /// # + /// # Ok(protoset) + /// # } + /// # fn main() { do_test(); } // wrap the test so we can use the ? operator + /// ``` + pub fn contains(&self, version: &Version) -> bool { + for &(low, high) in self.iter() { + if low <= *version && *version <= high { + return true; + } + } + false + } + + /// Retain only the `Version`s in this `ProtoSet` for which the predicate + /// `F` returns `true`. + /// + /// # Examples + /// + /// ``` + /// # use protover::errors::ProtoverError; + /// use protover::protoset::ProtoSet; + /// + /// # fn do_test() -> Result<bool, ProtoverError> { + /// let mut protoset: ProtoSet = "1,3-5,9".parse()?; + /// + /// // Keep only versions less than or equal to 8: + /// protoset.retain(|x| x <= &8); + /// + /// assert_eq!(protoset.expand(), vec![1, 3, 4, 5]); + /// # + /// # Ok(true) + /// # } + /// # fn main() { do_test(); } // wrap the test so we can use the ? operator + /// ``` + // XXX we could probably do something more efficient here. —isis + pub fn retain<F>(&mut self, f: F) + where F: FnMut(&Version) -> bool + { + let mut expanded: Vec<Version> = self.clone().expand(); + expanded.retain(f); + *self = expanded.into(); + } +} + +impl FromStr for ProtoSet { + type Err = ProtoverError; + + /// Parse the unique version numbers supported by a subprotocol from a string. + /// + /// # Inputs + /// + /// * `version_string`, a string comprised of "[0-9,-]" + /// + /// # Returns + /// + /// A `Result` whose `Ok` value is a `ProtoSet` holding all of the unique + /// version numbers. + /// + /// The returned `Result`'s `Err` value is an `ProtoverError` appropriate to + /// the error. + /// + /// # Errors + /// + /// This function will error if: + /// + /// * the `version_string` is an equals (`"="`) sign, + /// * the expansion of a version range produces an error (see + /// `expand_version_range`), + /// * any single version number is not parseable as an `u32` in radix 10, or + /// * there are greater than 2^16 version numbers to expand. + /// + /// # Examples + /// + /// ``` + /// use std::str::FromStr; + /// + /// use protover::errors::ProtoverError; + /// use protover::protoset::ProtoSet; + /// + /// # fn do_test() -> Result<ProtoSet, ProtoverError> { + /// let protoset: ProtoSet = ProtoSet::from_str("2-5,8")?; + /// + /// assert!(protoset.contains(&5)); + /// assert!(!protoset.contains(&10)); + /// + /// // We can also equivalently call `ProtoSet::from_str` by doing (all + /// // implementations of `FromStr` can be called this way, this one isn't + /// // special): + /// let protoset: ProtoSet = "4-6,12".parse()?; + /// + /// // Calling it (either way) can take really large ranges (up to `u32::MAX`): + /// let protoset: ProtoSet = "1-70000".parse()?; + /// let protoset: ProtoSet = "1-4294967296".parse()?; + /// + /// // There are lots of ways to get an `Err` from this function. Here are + /// // a few: + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("=")); + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("-")); + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("not_an_int")); + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("3-")); + /// assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1-,4")); + /// + /// // Things which would get parsed into an _empty_ `ProtoSet` are, + /// // however, legal, and result in an empty `ProtoSet`: + /// assert_eq!(Ok(ProtoSet::default()), ProtoSet::from_str("")); + /// assert_eq!(Ok(ProtoSet::default()), ProtoSet::from_str(",,,")); + /// # + /// # Ok(protoset) + /// # } + /// # fn main() { do_test(); } // wrap the test so we can use the ? operator + /// ``` + fn from_str(version_string: &str) -> Result<Self, Self::Err> { + let mut pairs: Vec<(Version, Version)> = Vec::new(); + let pieces: ::std::str::Split<char> = version_string.trim().split(','); + + for piece in pieces { + let p: &str = piece.trim(); + + if p.is_empty() { + continue; + } else if p.contains('-') { + let mut pair = p.split('-'); + + let low = pair.next().ok_or(ProtoverError::Unparseable)?; + let high = pair.next().ok_or(ProtoverError::Unparseable)?; + + let lo: Version = low.parse().or(Err(ProtoverError::Unparseable))?; + let hi: Version = high.parse().or(Err(ProtoverError::Unparseable))?; + + if lo == u32::MAX || hi == u32::MAX { + return Err(ProtoverError::ExceedsMax); + } + pairs.push((lo, hi)); + } else { + let v: u32 = p.parse().or(Err(ProtoverError::Unparseable))?; + + if v == u32::MAX { + return Err(ProtoverError::ExceedsMax); + } + pairs.push((v, v)); + } + } + // If we were passed in an empty string, or a bunch of whitespace, or + // simply a comma, or a pile of commas, then return an empty ProtoSet. + if pairs.len() == 0 { + return Ok(ProtoSet::default()); + } + ProtoSet::from_slice(&pairs[..]) + } +} + +impl ToString for ProtoSet { + /// Contracts a `ProtoSet` of versions into a string. + /// + /// # Returns + /// + /// A `String` representation of this `ProtoSet` in ascending order. + fn to_string(&self) -> String { + let mut final_output: Vec<String> = Vec::new(); + + for &(lo, hi) in self.iter() { + if lo != hi { + debug_assert!(lo < hi); + final_output.push(format!("{}-{}", lo, hi)); + } else { + final_output.push(format!("{}", lo)); + } + } + final_output.join(",") + } +} + +/// Checks to see if there is a continuous range of integers, starting at the +/// first in the list. Returns the last integer in the range if a range exists. +/// +/// # Inputs +/// +/// `list`, an ordered vector of `u32` integers of "[0-9,-]" representing the +/// supported versions for a single protocol. +/// +/// # Returns +/// +/// A `bool` indicating whether the list contains a range, starting at the first +/// in the list, a`Version` of the last integer in the range, and a `usize` of +/// the index of that version. +/// +/// For example, if given vec![1, 2, 3, 5], find_range will return true, +/// as there is a continuous range, and 3, which is the last number in the +/// continuous range, and 2 which is the index of 3. +fn find_range(list: &Vec<Version>) -> (bool, Version, usize) { + if list.len() == 0 { + return (false, 0, 0); + } + + let mut index: usize = 0; + let mut iterable = list.iter().peekable(); + let mut range_end = match iterable.next() { + Some(n) => *n, + None => return (false, 0, 0), + }; + + let mut has_range = false; + + while iterable.peek().is_some() { + let n = *iterable.next().unwrap(); + if n != range_end + 1 { + break; + } + + has_range = true; + range_end = n; + index += 1; + } + + (has_range, range_end, index) +} + +impl From<Vec<Version>> for ProtoSet { + fn from(mut v: Vec<Version>) -> ProtoSet { + let mut version_pairs: Vec<(Version, Version)> = Vec::new(); + + v.sort_unstable(); + v.dedup(); + + 'vector: while !v.is_empty() { + let (has_range, end, index): (bool, Version, usize) = find_range(&v); + + if has_range { + let first: Version = match v.first() { + Some(x) => *x, + None => continue, + }; + let last: Version = match v.get(index) { + Some(x) => *x, + None => continue, + }; + debug_assert!(last == end, format!("last = {}, end = {}", last, end)); + + version_pairs.push((first, last)); + v = v.split_off(index + 1); + + if v.len() == 0 { + break 'vector; + } + } else { + let last: Version = match v.get(index) { + Some(x) => *x, + None => continue, + }; + version_pairs.push((last, last)); + v.remove(index); + } + } + ProtoSet::from_slice(&version_pairs[..]).unwrap_or(ProtoSet::default()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_find_range() { + assert_eq!((false, 0, 0), find_range(&vec![])); + assert_eq!((false, 1, 0), find_range(&vec![1])); + assert_eq!((true, 2, 1), find_range(&vec![1, 2])); + assert_eq!((true, 3, 2), find_range(&vec![1, 2, 3])); + assert_eq!((true, 3, 2), find_range(&vec![1, 2, 3, 5])); + } + + macro_rules! assert_contains_each { + ($protoset:expr, $versions:expr) => ( + for version in $versions { + assert!($protoset.contains(version)); + } + ) + } + + macro_rules! test_protoset_contains_versions { + ($list:expr, $str:expr) => ( + let versions: &[Version] = $list; + let protoset: Result<ProtoSet, ProtoverError> = ProtoSet::from_str($str); + + assert!(protoset.is_ok()); + let p = protoset.unwrap(); + assert_contains_each!(p, versions); + ) + } + + #[test] + fn test_versions_from_str() { + test_protoset_contains_versions!(&[], ""); + test_protoset_contains_versions!(&[1], "1"); + test_protoset_contains_versions!(&[1, 2], "1,2"); + test_protoset_contains_versions!(&[1, 2, 3], "1-3"); + test_protoset_contains_versions!(&[0, 1], "0-1"); + test_protoset_contains_versions!(&[1, 2, 5], "1-2,5"); + test_protoset_contains_versions!(&[1, 3, 4, 5], "1,3-5"); + test_protoset_contains_versions!(&[42, 55, 56, 57, 58], "42,55-58"); + } + + #[test] + fn test_versions_from_str_ab() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("a,b")); + } + + #[test] + fn test_versions_from_str_negative_1() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("-1")); + } + + #[test] + fn test_versions_from_str_1exclam() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("1,!")); + } + + #[test] + fn test_versions_from_str_percent_equal() { + assert_eq!(Err(ProtoverError::Unparseable), ProtoSet::from_str("%=")); + } + + #[test] + fn test_versions_from_str_overlap() { + assert_eq!(Err(ProtoverError::Overlap), ProtoSet::from_str("1-3,2-4")); + } + + #[test] + fn test_versions_from_slice_overlap() { + assert_eq!(Err(ProtoverError::Overlap), ProtoSet::from_slice(&[(1, 3), (2, 4)])); + } + + #[test] + fn test_versions_from_str_max() { + assert_eq!(Err(ProtoverError::ExceedsMax), ProtoSet::from_str("4294967295")); + } + + #[test] + fn test_versions_from_slice_max() { + assert_eq!(Err(ProtoverError::ExceedsMax), ProtoSet::from_slice(&[(4294967295, 4294967295)])); + } + + #[test] + fn test_protoset_contains() { + let protoset: ProtoSet = ProtoSet::from_slice(&[(0, 5), (7, 9), (13, 14)]).unwrap(); + + for x in 0..6 { assert!(protoset.contains(&x), format!("should contain {}", x)); } + for x in 7..10 { assert!(protoset.contains(&x), format!("should contain {}", x)); } + for x in 13..15 { assert!(protoset.contains(&x), format!("should contain {}", x)); } + + for x in [6, 10, 11, 12, 15, 42, 43, 44, 45, 1234584].iter() { + assert!(!protoset.contains(&x), format!("should not contain {}", x)); + } + } + + #[test] + fn test_protoset_contains_0_3() { + let protoset: ProtoSet = ProtoSet::from_slice(&[(0, 3)]).unwrap(); + + for x in 0..4 { assert!(protoset.contains(&x), format!("should contain {}", x)); } + } + + macro_rules! assert_protoset_from_vec_contains_all { + ($($x:expr),*) => ( + let vec: Vec<Version> = vec!($($x),*); + let protoset: ProtoSet = vec.clone().into(); + + for x in vec.iter() { + assert!(protoset.contains(&x)); + } + ) + } + + #[test] + fn test_protoset_from_vec_123() { + assert_protoset_from_vec_contains_all!(1, 2, 3); + } + + #[test] + fn test_protoset_from_vec_0_315() { + assert_protoset_from_vec_contains_all!(0, 1, 2, 3, 15); + } + + #[test] + fn test_protoset_from_vec_unordered() { + let v: Vec<Version> = vec!(2, 3, 8, 4, 3, 9, 7, 2); + let ps: ProtoSet = v.into(); + + assert_eq!(ps.to_string(), "2-4,7-9"); + } + + #[test] + fn test_protoset_into_vec() { + let ps: ProtoSet = "1-13,42,9001,4294967294".parse().unwrap(); + let v: Vec<Version> = ps.into(); + + assert!(v.contains(&7)); + assert!(v.contains(&9001)); + assert!(v.contains(&4294967294)); + } +} + +#[cfg(all(test, feature = "bench"))] +mod bench { + use super::*; +} diff --git a/src/rust/protover/protover.rs b/src/rust/protover/protover.rs new file mode 100644 index 0000000000..17a8d60ec6 --- /dev/null +++ b/src/rust/protover/protover.rs @@ -0,0 +1,899 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +use std::collections::HashMap; +use std::collections::hash_map; +use std::fmt; +use std::str; +use std::str::FromStr; +use std::string::String; + +use tor_util::strings::NUL_BYTE; +use external::c_tor_version_as_new_as; + +use errors::ProtoverError; +use protoset::Version; +use protoset::ProtoSet; + +/// The first version of Tor that included "proto" entries in its descriptors. +/// Authorities should use this to decide whether to guess proto lines. +/// +/// C_RUST_COUPLED: +/// src/or/protover.h `FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS` +const FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS: &'static str = "0.2.9.3-alpha"; + +/// The maximum number of subprotocol version numbers we will attempt to expand +/// before concluding that someone is trying to DoS us +/// +/// C_RUST_COUPLED: src/or/protover.c `MAX_PROTOCOLS_TO_EXPAND` +const MAX_PROTOCOLS_TO_EXPAND: usize = (1<<16); + +/// The maximum size an `UnknownProtocol`'s name may be. +pub(crate) const MAX_PROTOCOL_NAME_LENGTH: usize = 100; + +/// Currently supported protocols and their versions, as a byte-slice. +/// +/// # Warning +/// +/// This byte-slice ends in a NUL byte. This is so that we can directly convert +/// it to an `&'static CStr` in the FFI code, in order to hand the static string +/// to C in a way that is compatible with C static strings. +/// +/// Rust code which wishes to accesses this string should use +/// `protover::get_supported_protocols()` instead. +/// +/// C_RUST_COUPLED: src/or/protover.c `protover_get_supported_protocols` +pub(crate) const SUPPORTED_PROTOCOLS: &'static [u8] = + b"Cons=1-2 \ + Desc=1-2 \ + DirCache=1-2 \ + HSDir=1-2 \ + HSIntro=3-4 \ + HSRend=1-2 \ + Link=1-5 \ + LinkAuth=1,3 \ + Microdesc=1-2 \ + Relay=1-2\0"; + +/// Known subprotocols in Tor. Indicates which subprotocol a relay supports. +/// +/// C_RUST_COUPLED: src/or/protover.h `protocol_type_t` +#[derive(Clone, Hash, Eq, PartialEq, Debug)] +pub enum Protocol { + Cons, + Desc, + DirCache, + HSDir, + HSIntro, + HSRend, + Link, + LinkAuth, + Microdesc, + Relay, +} + +impl fmt::Display for Protocol { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Translates a string representation of a protocol into a Proto type. +/// Error if the string is an unrecognized protocol name. +/// +/// C_RUST_COUPLED: src/or/protover.c `PROTOCOL_NAMES` +impl FromStr for Protocol { + type Err = ProtoverError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "Cons" => Ok(Protocol::Cons), + "Desc" => Ok(Protocol::Desc), + "DirCache" => Ok(Protocol::DirCache), + "HSDir" => Ok(Protocol::HSDir), + "HSIntro" => Ok(Protocol::HSIntro), + "HSRend" => Ok(Protocol::HSRend), + "Link" => Ok(Protocol::Link), + "LinkAuth" => Ok(Protocol::LinkAuth), + "Microdesc" => Ok(Protocol::Microdesc), + "Relay" => Ok(Protocol::Relay), + _ => Err(ProtoverError::UnknownProtocol), + } + } +} + +/// A protocol string which is not one of the `Protocols` we currently know +/// about. +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct UnknownProtocol(String); + +impl fmt::Display for UnknownProtocol { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for UnknownProtocol { + type Err = ProtoverError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.len() <= MAX_PROTOCOL_NAME_LENGTH { + Ok(UnknownProtocol(s.to_string())) + } else { + Err(ProtoverError::ExceedsNameLimit) + } + } +} + +impl UnknownProtocol { + /// Create an `UnknownProtocol`, ignoring whether or not it + /// exceeds MAX_PROTOCOL_NAME_LENGTH. + fn from_str_any_len(s: &str) -> Result<Self, ProtoverError> { + Ok(UnknownProtocol(s.to_string())) + } +} + +impl From<Protocol> for UnknownProtocol { + fn from(p: Protocol) -> UnknownProtocol { + UnknownProtocol(p.to_string()) + } +} + +/// Get the string representation of current supported protocols +/// +/// # Returns +/// +/// A `String` whose value is the existing protocols supported by tor. +/// Returned data is in the format as follows: +/// +/// "HSDir=1-1 LinkAuth=1" +/// +pub fn get_supported_protocols() -> &'static str { + // The `len() - 1` is to remove the NUL byte. + // The `unwrap` is safe becauase we SUPPORTED_PROTOCOLS is under + // our control. + str::from_utf8(&SUPPORTED_PROTOCOLS[..SUPPORTED_PROTOCOLS.len() - 1]) + .unwrap_or("") +} + +/// A map of protocol names to the versions of them which are supported. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProtoEntry(HashMap<Protocol, ProtoSet>); + +impl Default for ProtoEntry { + fn default() -> ProtoEntry { + ProtoEntry( HashMap::new() ) + } +} + +impl ProtoEntry { + /// Get an iterator over the `Protocol`s and their `ProtoSet`s in this `ProtoEntry`. + pub fn iter(&self) -> hash_map::Iter<Protocol, ProtoSet> { + self.0.iter() + } + + /// Translate the supported tor versions from a string into a + /// ProtoEntry, which is useful when looking up a specific + /// subprotocol. + pub fn supported() -> Result<Self, ProtoverError> { + let supported: &'static str = get_supported_protocols(); + + supported.parse() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn get(&self, protocol: &Protocol) -> Option<&ProtoSet> { + self.0.get(protocol) + } + + pub fn insert(&mut self, key: Protocol, value: ProtoSet) { + self.0.insert(key, value); + } + + pub fn remove(&mut self, key: &Protocol) -> Option<ProtoSet> { + self.0.remove(key) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl FromStr for ProtoEntry { + type Err = ProtoverError; + + /// Parse a string of subprotocol types and their version numbers. + /// + /// # Inputs + /// + /// * A `protocol_entry` string, comprised of a keywords, an "=" sign, and + /// one or more version numbers, each separated by a space. For example, + /// `"Cons=3-4 HSDir=1"`. + /// + /// # Returns + /// + /// A `Result` whose `Ok` value is a `ProtoEntry`, where the + /// first element is the subprotocol type (see `protover::Protocol`) and the last + /// element is an ordered set of `(low, high)` unique version numbers which are supported. + /// Otherwise, the `Err` value of this `Result` is a `ProtoverError`. + fn from_str(protocol_entry: &str) -> Result<ProtoEntry, ProtoverError> { + let mut proto_entry: ProtoEntry = ProtoEntry::default(); + let entries = protocol_entry.split(' '); + + for entry in entries { + let mut parts = entry.splitn(2, '='); + + let proto = match parts.next() { + Some(n) => n, + None => return Err(ProtoverError::Unparseable), + }; + + let vers = match parts.next() { + Some(n) => n, + None => return Err(ProtoverError::Unparseable), + }; + let versions: ProtoSet = vers.parse()?; + let proto_name: Protocol = proto.parse()?; + + proto_entry.insert(proto_name, versions); + + if proto_entry.len() > MAX_PROTOCOLS_TO_EXPAND { + return Err(ProtoverError::ExceedsMax); + } + } + Ok(proto_entry) + } +} + +/// Generate an implementation of `ToString` for either a `ProtoEntry` or an +/// `UnvalidatedProtoEntry`. +macro_rules! impl_to_string_for_proto_entry { + ($t:ty) => ( + impl ToString for $t { + fn to_string(&self) -> String { + let mut parts: Vec<String> = Vec::new(); + + for (protocol, versions) in self.iter() { + parts.push(format!("{}={}", protocol.to_string(), versions.to_string())); + } + parts.sort_unstable(); + parts.join(" ") + } + } + ) +} + +impl_to_string_for_proto_entry!(ProtoEntry); +impl_to_string_for_proto_entry!(UnvalidatedProtoEntry); + +/// A `ProtoEntry`, but whose `Protocols` can be any `UnknownProtocol`, not just +/// the supported ones enumerated in `Protocols`. The protocol versions are +/// validated, however. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UnvalidatedProtoEntry(HashMap<UnknownProtocol, ProtoSet>); + +impl Default for UnvalidatedProtoEntry { + fn default() -> UnvalidatedProtoEntry { + UnvalidatedProtoEntry( HashMap::new() ) + } +} + +impl UnvalidatedProtoEntry { + /// Get an iterator over the `Protocol`s and their `ProtoSet`s in this `ProtoEntry`. + pub fn iter(&self) -> hash_map::Iter<UnknownProtocol, ProtoSet> { + self.0.iter() + } + + pub fn get(&self, protocol: &UnknownProtocol) -> Option<&ProtoSet> { + self.0.get(protocol) + } + + pub fn insert(&mut self, key: UnknownProtocol, value: ProtoSet) { + self.0.insert(key, value); + } + + pub fn remove(&mut self, key: &UnknownProtocol) -> Option<ProtoSet> { + self.0.remove(key) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn len(&self) -> usize { + let mut total: usize = 0; + + for (_, versions) in self.iter() { + total += versions.len(); + } + total + } + + /// Determine if we support every protocol a client supports, and if not, + /// determine which protocols we do not have support for. + /// + /// # Returns + /// + /// Optionally, return parameters which the client supports but which we do not. + /// + /// # Examples + /// ``` + /// use protover::UnvalidatedProtoEntry; + /// + /// let protocols: UnvalidatedProtoEntry = "LinkAuth=1 Microdesc=1-2 Relay=2".parse().unwrap(); + /// let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + /// assert_eq!(true, unsupported.is_none()); + /// + /// let protocols: UnvalidatedProtoEntry = "Link=1-2 Wombat=9".parse().unwrap(); + /// let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + /// assert_eq!(true, unsupported.is_some()); + /// assert_eq!("Wombat=9", &unsupported.unwrap().to_string()); + /// ``` + pub fn all_supported(&self) -> Option<UnvalidatedProtoEntry> { + let mut unsupported: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + let supported: ProtoEntry = match ProtoEntry::supported() { + Ok(x) => x, + Err(_) => return None, + }; + + for (protocol, versions) in self.iter() { + let is_supported: Result<Protocol, ProtoverError> = protocol.0.parse(); + let supported_protocol: Protocol; + + // If the protocol wasn't even in the enum, then we definitely don't + // know about it and don't support any of its versions. + if is_supported.is_err() { + if !versions.is_empty() { + unsupported.insert(protocol.clone(), versions.clone()); + } + continue; + } else { + supported_protocol = is_supported.unwrap(); + } + + let maybe_supported_versions: Option<&ProtoSet> = supported.get(&supported_protocol); + let supported_versions: &ProtoSet; + let mut unsupported_versions: ProtoSet; + + // If the protocol wasn't in the map, then we don't know about it + // and don't support any of its versions. Add its versions to the + // map (if it has versions). + if maybe_supported_versions.is_none() { + if !versions.is_empty() { + unsupported.insert(protocol.clone(), versions.clone()); + } + continue; + } else { + supported_versions = maybe_supported_versions.unwrap(); + } + unsupported_versions = versions.clone(); + unsupported_versions.retain(|x| !supported_versions.contains(x)); + + if !unsupported_versions.is_empty() { + unsupported.insert(protocol.clone(), unsupported_versions); + } + } + + if unsupported.is_empty() { + return None; + } + Some(unsupported) + } + + /// Determine if we have support for some protocol and version. + /// + /// # Inputs + /// + /// * `proto`, an `UnknownProtocol` to test support for + /// * `vers`, a `Version` which we will go on to determine whether the + /// specified protocol supports. + /// + /// # Return + /// + /// Returns `true` iff this `UnvalidatedProtoEntry` includes support for the + /// indicated protocol and version, and `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use std::str::FromStr; + /// use protover::*; + /// # use protover::errors::ProtoverError; + /// + /// # fn do_test () -> Result<UnvalidatedProtoEntry, ProtoverError> { + /// let proto: UnvalidatedProtoEntry = "Link=3-4 Cons=1 Doggo=3-5".parse()?; + /// assert_eq!(true, proto.supports_protocol(&Protocol::Cons.into(), &1)); + /// assert_eq!(false, proto.supports_protocol(&Protocol::Cons.into(), &5)); + /// assert_eq!(true, proto.supports_protocol(&UnknownProtocol::from_str("Doggo")?, &4)); + /// # Ok(proto) + /// # } fn main () { do_test(); } + /// ``` + pub fn supports_protocol(&self, proto: &UnknownProtocol, vers: &Version) -> bool { + let supported_versions: &ProtoSet = match self.get(proto) { + Some(n) => n, + None => return false, + }; + supported_versions.contains(&vers) + } + + /// As `UnvalidatedProtoEntry::supports_protocol()`, but also returns `true` + /// if any later version of the protocol is supported. + /// + /// # Examples + /// ``` + /// use protover::*; + /// # use protover::errors::ProtoverError; + /// + /// # fn do_test () -> Result<UnvalidatedProtoEntry, ProtoverError> { + /// let proto: UnvalidatedProtoEntry = "Link=3-4 Cons=5".parse()?; + /// + /// assert_eq!(true, proto.supports_protocol_or_later(&Protocol::Cons.into(), &5)); + /// assert_eq!(true, proto.supports_protocol_or_later(&Protocol::Cons.into(), &4)); + /// assert_eq!(false, proto.supports_protocol_or_later(&Protocol::Cons.into(), &6)); + /// # Ok(proto) + /// # } fn main () { do_test(); } + /// ``` + pub fn supports_protocol_or_later(&self, proto: &UnknownProtocol, vers: &Version) -> bool { + let supported_versions: &ProtoSet = match self.get(&proto) { + Some(n) => n, + None => return false, + }; + supported_versions.iter().any(|v| v.1 >= *vers) + } + + /// Split a string containing (potentially) several protocols and their + /// versions into a `Vec` of tuples of string in `(protocol, versions)` + /// form. + /// + /// # Inputs + /// + /// A &str in the form `"Link=3-4 Cons=5"`. + /// + /// # Returns + /// + /// A `Result` whose `Ok` variant is a `Vec<(&str, &str)>` of `(protocol, + /// versions)`, or whose `Err` variant is a `ProtoverError`. + /// + /// # Errors + /// + /// This will error with a `ProtoverError::Unparseable` if any of the + /// following are true: + /// + /// * If a protocol name is an empty string, e.g. `"Cons=1,3 =3-5"`. + /// * If a protocol name cannot be parsed as utf-8. + /// * If the version numbers are an empty string, e.g. `"Cons="`. + fn parse_protocol_and_version_str<'a>(protocol_string: &'a str) + -> Result<Vec<(&'a str, &'a str)>, ProtoverError> + { + let mut protovers: Vec<(&str, &str)> = Vec::new(); + + for subproto in protocol_string.split(' ') { + let mut parts = subproto.splitn(2, '='); + + let name = match parts.next() { + Some("") => return Err(ProtoverError::Unparseable), + Some(n) => n, + None => return Err(ProtoverError::Unparseable), + }; + let vers = match parts.next() { + Some(n) => n, + None => return Err(ProtoverError::Unparseable), + }; + protovers.push((name, vers)); + } + Ok(protovers) + } +} + +impl FromStr for UnvalidatedProtoEntry { + type Err = ProtoverError; + + /// Parses a protocol list without validating the protocol names. + /// + /// # Inputs + /// + /// * `protocol_string`, a string comprised of keys and values, both which are + /// strings. The keys are the protocol names while values are a string + /// representation of the supported versions. + /// + /// The input is _not_ expected to be a subset of the Protocol types + /// + /// # Returns + /// + /// A `Result` whose `Ok` value is a `ProtoSet` holding all of the + /// unique version numbers. + /// + /// The returned `Result`'s `Err` value is an `ProtoverError` whose `Display` + /// impl has a description of the error. + /// + /// # Errors + /// + /// This function will error if: + /// + /// * The protocol string does not follow the "protocol_name=version_list" + /// expected format, or + /// * If the version string is malformed. See `impl FromStr for ProtoSet`. + fn from_str(protocol_string: &str) -> Result<UnvalidatedProtoEntry, ProtoverError> { + let mut parsed: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + let parts: Vec<(&str, &str)> = + UnvalidatedProtoEntry::parse_protocol_and_version_str(protocol_string)?; + + for &(name, vers) in parts.iter() { + let versions = ProtoSet::from_str(vers)?; + let protocol = UnknownProtocol::from_str(name)?; + + parsed.insert(protocol, versions); + } + Ok(parsed) + } +} + +impl UnvalidatedProtoEntry { + /// Create an `UnknownProtocol`, ignoring whether or not it + /// exceeds MAX_PROTOCOL_NAME_LENGTH. + pub(crate) fn from_str_any_len(protocol_string: &str) + -> Result<UnvalidatedProtoEntry, ProtoverError> + { + let mut parsed: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + let parts: Vec<(&str, &str)> = + UnvalidatedProtoEntry::parse_protocol_and_version_str(protocol_string)?; + + for &(name, vers) in parts.iter() { + let versions = ProtoSet::from_str(vers)?; + let protocol = UnknownProtocol::from_str_any_len(name)?; + + parsed.insert(protocol, versions); + } + Ok(parsed) + } +} + +/// Pretend a `ProtoEntry` is actually an `UnvalidatedProtoEntry`. +impl From<ProtoEntry> for UnvalidatedProtoEntry { + fn from(proto_entry: ProtoEntry) -> UnvalidatedProtoEntry { + let mut unvalidated: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + + for (protocol, versions) in proto_entry.iter() { + unvalidated.insert(UnknownProtocol::from(protocol.clone()), versions.clone()); + } + unvalidated + } +} + +/// A mapping of protocols to a count of how many times each of their `Version`s +/// were voted for or supported. +/// +/// # Warning +/// +/// The "protocols" are *not* guaranteed to be known/supported `Protocol`s, in +/// order to allow new subprotocols to be introduced even if Directory +/// Authorities don't yet know of them. +pub struct ProtoverVote( HashMap<UnknownProtocol, HashMap<Version, usize>> ); + +impl Default for ProtoverVote { + fn default() -> ProtoverVote { + ProtoverVote( HashMap::new() ) + } +} + +impl IntoIterator for ProtoverVote { + type Item = (UnknownProtocol, HashMap<Version, usize>); + type IntoIter = hash_map::IntoIter<UnknownProtocol, HashMap<Version, usize>>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl ProtoverVote { + pub fn entry(&mut self, key: UnknownProtocol) + -> hash_map::Entry<UnknownProtocol, HashMap<Version, usize>> + { + self.0.entry(key) + } + + /// Protocol voting implementation. + /// + /// Given a slice of `UnvalidatedProtoEntry`s and a vote `threshold`, return + /// a new `UnvalidatedProtoEntry` encoding all of the protocols that are + /// listed by at least `threshold` of the inputs. + /// + /// # Examples + /// + /// ``` + /// use protover::ProtoverVote; + /// use protover::UnvalidatedProtoEntry; + /// + /// let protos: &[UnvalidatedProtoEntry] = &["Link=3-4".parse().unwrap(), + /// "Link=3".parse().unwrap()]; + /// let vote = ProtoverVote::compute(protos, &2); + /// assert_eq!("Link=3", vote.to_string()); + /// ``` + // C_RUST_COUPLED: /src/or/protover.c protover_compute_vote + pub fn compute(proto_entries: &[UnvalidatedProtoEntry], threshold: &usize) -> UnvalidatedProtoEntry { + let mut all_count: ProtoverVote = ProtoverVote::default(); + let mut final_output: UnvalidatedProtoEntry = UnvalidatedProtoEntry::default(); + + if proto_entries.is_empty() { + return final_output; + } + + // parse and collect all of the protos and their versions and collect them + for vote in proto_entries { + // C_RUST_DIFFERS: This doesn't actually differ, bu this check on + // the total is here to make it match. Because the C version calls + // expand_protocol_list() which checks if there would be too many + // subprotocols *or* individual version numbers, i.e. more than + // MAX_PROTOCOLS_TO_EXPAND, and does this *per vote*, we need to + // match it's behaviour and ensure we're not allowing more than it + // would. + if vote.len() > MAX_PROTOCOLS_TO_EXPAND { + continue; + } + + for (protocol, versions) in vote.iter() { + let supported_vers: &mut HashMap<Version, usize> = + all_count.entry(protocol.clone()).or_insert(HashMap::new()); + + for version in versions.clone().expand() { + let counter: &mut usize = + supported_vers.entry(version).or_insert(0); + *counter += 1; + } + } + } + + for (protocol, mut versions) in all_count { + // Go through and remove versions that are less than the threshold + versions.retain(|_, count| *count as usize >= *threshold); + + if versions.len() > 0 { + let voted_versions: Vec<Version> = versions.keys().cloned().collect(); + let voted_protoset: ProtoSet = ProtoSet::from(voted_versions); + + final_output.insert(protocol, voted_protoset); + } + } + final_output + } +} + +/// Returns a boolean indicating whether the given protocol and version is +/// supported in any of the existing Tor protocols +/// +/// # Examples +/// ``` +/// use protover::is_supported_here; +/// use protover::Protocol; +/// +/// let is_supported = is_supported_here(&Protocol::Link, &10); +/// assert_eq!(false, is_supported); +/// +/// let is_supported = is_supported_here(&Protocol::Link, &1); +/// assert_eq!(true, is_supported); +/// ``` +pub fn is_supported_here(proto: &Protocol, vers: &Version) -> bool { + let currently_supported: ProtoEntry = match ProtoEntry::supported() { + Ok(result) => result, + Err(_) => return false, + }; + let supported_versions = match currently_supported.get(proto) { + Some(n) => n, + None => return false, + }; + supported_versions.contains(vers) +} + +/// Since older versions of Tor cannot infer their own subprotocols, +/// determine which subprotocols are supported by older Tor versions. +/// +/// # Inputs +/// +/// * `version`, a string comprised of "[0-9a-z.-]" +/// +/// # Returns +/// +/// A `&'static [u8]` encoding a list of protocol names and supported +/// versions. The string takes the following format: +/// +/// "HSDir=1-1 LinkAuth=1" +/// +/// This function returns the protocols that are supported by the version input, +/// only for tor versions older than `FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS` +/// (but not older than 0.2.4.19). For newer tors (or older than 0.2.4.19), it +/// returns an empty string. +/// +/// # Note +/// +/// This function is meant to be called for/within FFI code. If you'd +/// like to use this code in Rust, please see `compute_for_old_tor()`. +// +// C_RUST_COUPLED: src/rust/protover.c `compute_for_old_tor` +pub(crate) fn compute_for_old_tor_cstr(version: &str) -> &'static [u8] { + if c_tor_version_as_new_as(version, FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS) { + return NUL_BYTE; + } + if c_tor_version_as_new_as(version, "0.2.9.1-alpha") { + return b"Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1-2 \ + Link=1-4 LinkAuth=1 Microdesc=1-2 Relay=1-2\0"; + } + if c_tor_version_as_new_as(version, "0.2.7.5") { + return b"Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 \ + Link=1-4 LinkAuth=1 Microdesc=1-2 Relay=1-2\0"; + } + if c_tor_version_as_new_as(version, "0.2.4.19") { + return b"Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 \ + Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2\0"; + } + + NUL_BYTE +} + +/// Since older versions of Tor cannot infer their own subprotocols, +/// determine which subprotocols are supported by older Tor versions. +/// +/// # Inputs +/// +/// * `version`, a string comprised of "[0-9a-z.-]" +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is an `&'static str` encoding a list of protocol +/// names and supported versions. The string takes the following format: +/// +/// "HSDir=1-1 LinkAuth=1" +/// +/// This function returns the protocols that are supported by the version input, +/// only for tor versions older than `FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS`. +/// (but not older than 0.2.4.19). For newer tors (or older than 0.2.4.19), its +/// `Ok` `Result` contains an empty string. +/// +/// Otherwise, its `Err` contains a `ProtoverError::Unparseable` if the +/// `version` string was invalid utf-8. +/// +/// # Note +/// +/// This function is meant to be called for/within non-FFI Rust code. +// +// C_RUST_COUPLED: src/rust/protover.c `compute_for_old_tor` +pub fn compute_for_old_tor(version: &str) -> Result<&'static str, ProtoverError> { + let mut computed: &'static [u8] = compute_for_old_tor_cstr(version); + + // Remove the NULL byte at the end. + computed = &computed[..computed.len() - 1]; + + // .from_utf8() fails with a Utf8Error if it couldn't validate the + // utf-8, so convert that here into an Unparseable ProtoverError. + str::from_utf8(computed).or(Err(ProtoverError::Unparseable)) +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + use std::string::ToString; + + use super::*; + + macro_rules! assert_protoentry_is_parseable { + ($e:expr) => ( + let protoentry: Result<ProtoEntry, ProtoverError> = $e.parse(); + + assert!(protoentry.is_ok(), format!("{:?}", protoentry.err())); + ) + } + + macro_rules! assert_protoentry_is_unparseable { + ($e:expr) => ( + let protoentry: Result<ProtoEntry, ProtoverError> = $e.parse(); + + assert!(protoentry.is_err()); + ) + } + + #[test] + fn test_protoentry_from_str_multiple_protocols_multiple_versions() { + assert_protoentry_is_parseable!("Cons=3-4 Link=1,3-5"); + } + + #[test] + fn test_protoentry_from_str_empty() { + assert_protoentry_is_unparseable!(""); + } + + #[test] + fn test_protoentry_from_str_single_protocol_single_version() { + assert_protoentry_is_parseable!("HSDir=1"); + } + + #[test] + fn test_protoentry_from_str_unknown_protocol() { + assert_protoentry_is_unparseable!("Ducks=5-7,8"); + } + + #[test] + fn test_protoentry_from_str_allowed_number_of_versions() { + assert_protoentry_is_parseable!("Desc=1-4294967294"); + } + + #[test] + fn test_protoentry_from_str_too_many_versions() { + assert_protoentry_is_unparseable!("Desc=1-4294967295"); + } + + #[test] + fn test_protoentry_from_str_() { + assert_protoentry_is_unparseable!(""); + } + + #[test] + fn test_protoentry_all_supported_single_protocol_single_version() { + let protocol: UnvalidatedProtoEntry = "Cons=1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); + } + + #[test] + fn test_protoentry_all_supported_multiple_protocol_multiple_versions() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Desc=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); + } + + #[test] + fn test_protoentry_all_supported_three_values() { + let protocols: UnvalidatedProtoEntry = "LinkAuth=1 Microdesc=1-2 Relay=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); + } + + #[test] + fn test_protoentry_all_supported_unknown_protocol() { + let protocols: UnvalidatedProtoEntry = "Wombat=9".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Wombat=9", &unsupported.unwrap().to_string()); + } + + #[test] + fn test_protoentry_all_supported_unsupported_high_version() { + let protocols: UnvalidatedProtoEntry = "HSDir=12-100".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("HSDir=12-100", &unsupported.unwrap().to_string()); + } + + #[test] + fn test_protoentry_all_supported_unsupported_low_version() { + let protocols: UnvalidatedProtoEntry = "Cons=0-1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Cons=0", &unsupported.unwrap().to_string()); + } + + #[test] + fn test_contract_protocol_list() { + let mut versions = ""; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1-2"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1,3"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1-4"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1,3,5-7"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + + versions = "1-3,500"; + assert_eq!(String::from(versions), ProtoSet::from_str(&versions).unwrap().to_string()); + } +} diff --git a/src/rust/protover/tests/protover.rs b/src/rust/protover/tests/protover.rs new file mode 100644 index 0000000000..2db01a1634 --- /dev/null +++ b/src/rust/protover/tests/protover.rs @@ -0,0 +1,394 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +extern crate protover; + +use protover::ProtoEntry; +use protover::ProtoverVote; +use protover::UnvalidatedProtoEntry; +use protover::errors::ProtoverError; + +#[test] +fn parse_protocol_with_single_proto_and_single_version() { + let _: ProtoEntry = "Cons=1".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_multiple_versions() { + let _: ProtoEntry = "Cons=1-2".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_different_single_protocol_and_single_version() { + let _: ProtoEntry = "HSDir=1".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_supported_version() { + let _: ProtoEntry = "Desc=2".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_two_protocols_and_single_version() { + let _: ProtoEntry = "Cons=1 HSDir=1".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_two_sequential_versions() { + let _: ProtoEntry = "Desc=1-2".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_protocol_range() { + let _: ProtoEntry = "Link=1-4".parse().unwrap(); +} + +#[test] +fn parse_protocol_with_single_protocol_and_protocol_set() { + let _: ProtoEntry = "Link=3-4 Desc=2".parse().unwrap(); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_protocol_set() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Desc=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_two_values() { + let protocols: UnvalidatedProtoEntry = "Microdesc=1-2 Relay=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_one_value() { + let protocols: UnvalidatedProtoEntry = "Microdesc=1-2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +#[should_panic] +fn parse_protocol_unvalidated_with_empty() { + let _: UnvalidatedProtoEntry = "".parse().unwrap(); +} + +#[test] +#[should_panic] +fn parse_protocol_validated_with_empty() { + let _: UnvalidatedProtoEntry = "".parse().unwrap(); +} + +#[test] +fn protocol_all_supported_with_three_values() { + let protocols: UnvalidatedProtoEntry = "LinkAuth=1 Microdesc=1-2 Relay=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_unsupported_protocol() { + let protocols: UnvalidatedProtoEntry = "Wombat=9".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Wombat=9", &unsupported.unwrap().to_string()); +} + +#[test] +fn protocol_all_supported_with_unsupported_versions() { + let protocols: UnvalidatedProtoEntry = "Link=3-999".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Link=6-999", &unsupported.unwrap().to_string()); +} + +#[test] +fn protocol_all_supported_with_unsupported_low_version() { + let protocols: UnvalidatedProtoEntry = "Cons=0-1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Cons=0", &unsupported.unwrap().to_string()); +} + +#[test] +fn protocol_all_supported_with_unsupported_high_version() { + let protocols: UnvalidatedProtoEntry = "Cons=1-2,999".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Cons=999", &unsupported.unwrap().to_string()); +} + +#[test] +fn protocol_all_supported_with_mix_of_supported_and_unsupproted() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Wombat=9".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_some()); + assert_eq!("Wombat=9", &unsupported.unwrap().to_string()); +} + +#[test] +fn protover_string_supports_protocol_returns_true_for_single_supported() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Cons=1".parse().unwrap(); + let is_supported = protocols.supports_protocol(&protover::Protocol::Cons.into(), &1); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_string_supports_protocol_returns_false_for_single_unsupported() { + let protocols: UnvalidatedProtoEntry = "Link=3-4 Cons=1".parse().unwrap(); + let is_supported = protocols.supports_protocol(&protover::Protocol::Cons.into(), &2); + assert_eq!(false, is_supported); +} + +#[test] +fn protover_string_supports_protocol_returns_false_for_unsupported() { + let protocols: UnvalidatedProtoEntry = "Link=3-4".parse().unwrap(); + let is_supported = protocols.supports_protocol(&protover::Protocol::Cons.into(), &2); + assert_eq!(false, is_supported); +} + +#[test] +#[should_panic] +fn parse_protocol_with_unexpected_characters() { + let _: UnvalidatedProtoEntry = "Cons=*-%".parse().unwrap(); +} + +#[test] +#[should_panic] +fn protover_compute_vote_returns_empty_for_empty_string() { + let protocols: &[UnvalidatedProtoEntry] = &["".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_single_protocol_for_matching() { + let protocols: &[UnvalidatedProtoEntry] = &["Cons=1".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Cons=1", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_two_protocols_for_two_matching() { + let protocols: &[UnvalidatedProtoEntry] = &["Link=1 Cons=1".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Cons=1 Link=1", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_one_protocol_when_one_out_of_two_matches() { + let protocols: &[UnvalidatedProtoEntry] = &["Cons=1 Link=2".parse().unwrap(), "Cons=1".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &2); + assert_eq!("Cons=1", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_protocols_that_it_doesnt_currently_support() { + let protocols: &[UnvalidatedProtoEntry] = &["Foo=1 Cons=2".parse().unwrap(), "Bar=1".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Bar=1 Cons=2 Foo=1", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_matching_for_mix() { + let protocols: &[UnvalidatedProtoEntry] = &["Link=1-10,500 Cons=1,3-7,8".parse().unwrap()]; + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Cons=1,3-8 Link=1-10,500", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_matching_for_longer_mix() { + let protocols: &[UnvalidatedProtoEntry] = &[ + "Desc=1-10,500 Cons=1,3-7,8".parse().unwrap(), + "Link=123-456,78 Cons=2-6,8 Desc=9".parse().unwrap(), + ]; + + let listed = ProtoverVote::compute(protocols, &1); + assert_eq!("Cons=1-8 Desc=1-10,500 Link=78,123-456", listed.to_string()); +} + +#[test] +fn protover_compute_vote_returns_matching_for_longer_mix_with_threshold_two() { + let protocols: &[UnvalidatedProtoEntry] = &[ + "Desc=1-10,500 Cons=1,3-7,8".parse().unwrap(), + "Link=123-456,78 Cons=2-6,8 Desc=9".parse().unwrap(), + ]; + + let listed = ProtoverVote::compute(protocols, &2); + assert_eq!("Cons=3-6,8 Desc=9", listed.to_string()); +} + +#[test] +fn protover_compute_vote_handles_duplicated_versions() { + let protocols: &[UnvalidatedProtoEntry] = &["Cons=1".parse().unwrap(), "Cons=1".parse().unwrap()]; + assert_eq!("Cons=1", ProtoverVote::compute(protocols, &2).to_string()); + + let protocols: &[UnvalidatedProtoEntry] = &["Cons=1-2".parse().unwrap(), "Cons=1-2".parse().unwrap()]; + assert_eq!("Cons=1-2", ProtoverVote::compute(protocols, &2).to_string()); +} + +#[test] +fn protover_compute_vote_handles_invalid_proto_entries() { + let protocols: &[UnvalidatedProtoEntry] = &[ + "Cons=1".parse().unwrap(), + "Cons=1".parse().unwrap(), + "Dinosaur=1".parse().unwrap(), + ]; + assert_eq!("Cons=1", ProtoverVote::compute(protocols, &2).to_string()); +} + +#[test] +fn parse_protocol_with_single_protocol_and_two_nonsequential_versions() { + let _: ProtoEntry = "Desc=1,2".parse().unwrap(); +} + +#[test] +fn protover_is_supported_here_returns_true_for_supported_protocol() { + assert_eq!(true, protover::is_supported_here(&protover::Protocol::Cons, &1)); +} + +#[test] +fn protover_is_supported_here_returns_false_for_unsupported_protocol() { + assert_eq!(false, protover::is_supported_here(&protover::Protocol::Cons, &5)); +} + +#[test] +fn protocol_all_supported_with_single_proto_and_single_version() { + let protocol: UnvalidatedProtoEntry = "Cons=1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_multiple_versions() { + let protocol: UnvalidatedProtoEntry = "Cons=1-2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_different_single_protocol_and_single_version() { + let protocol: UnvalidatedProtoEntry = "HSDir=1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_supported_version() { + let protocol: UnvalidatedProtoEntry = "Desc=2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_two_protocols_and_single_version() { + let protocols: UnvalidatedProtoEntry = "Cons=1 HSDir=1".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_two_nonsequential_versions() { + let protocol: UnvalidatedProtoEntry = "Desc=1,2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_two_sequential_versions() { + let protocol: UnvalidatedProtoEntry = "Desc=1-2".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +#[test] +fn protocol_all_supported_with_single_protocol_and_protocol_range() { + let protocol: UnvalidatedProtoEntry = "Link=1-4".parse().unwrap(); + let unsupported: Option<UnvalidatedProtoEntry> = protocol.all_supported(); + assert_eq!(true, unsupported.is_none()); +} + +// By allowing us to add to votes, the C implementation allows us to +// exceed the limit. +#[test] +fn protover_compute_vote_may_exceed_limit() { + let proto1: UnvalidatedProtoEntry = "Sleen=1-65535".parse().unwrap(); + let proto2: UnvalidatedProtoEntry = "Sleen=100000".parse().unwrap(); + + let _result: UnvalidatedProtoEntry = ProtoverVote::compute(&[proto1, proto2], &1); +} + +#[test] +fn protover_all_supported_should_exclude_versions_we_actually_do_support() { + let proto: UnvalidatedProtoEntry = "Link=3-999".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=6-999".to_string()); +} + +#[test] +fn protover_all_supported_should_exclude_versions_we_actually_do_support_complex1() { + let proto: UnvalidatedProtoEntry = "Link=1-3,345-666".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=345-666".to_string()); +} + +#[test] +fn protover_all_supported_should_exclude_versions_we_actually_do_support_complex2() { + let proto: UnvalidatedProtoEntry = "Link=1-3,5-12".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=6-12".to_string()); +} + +#[test] +fn protover_all_supported_should_exclude_some_versions_and_entire_protocols() { + let proto: UnvalidatedProtoEntry = "Link=1-3,5-12 Quokka=9000-9001".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Link=6-12 Quokka=9000-9001".to_string()); +} + +#[test] +fn protover_all_supported_should_not_dos_anyones_computer() { + let proto: UnvalidatedProtoEntry = "Sleen=0-2147483648".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Sleen=0-2147483648".to_string()); +} + +#[test] +fn protover_all_supported_should_not_dos_anyones_computer_max_versions() { + let proto: UnvalidatedProtoEntry = "Sleen=0-4294967294".parse().unwrap(); + let result: String = proto.all_supported().unwrap().to_string(); + + assert_eq!(result, "Sleen=0-4294967294".to_string()); +} + +#[test] +// C_RUST_DIFFERS: The C will return true (e.g. saying "yes, that's supported") +// but set the msg to NULL (??? seems maybe potentially bad). The Rust will +// simply return a None. +fn protover_all_supported_should_return_empty_string_for_weird_thing() { + let proto: UnvalidatedProtoEntry = "Fribble=".parse().unwrap(); + let result: Option<UnvalidatedProtoEntry> = proto.all_supported(); + + assert!(result.is_none()); +} + +#[test] +fn protover_unvalidatedprotoentry_should_err_entirely_unparseable_things() { + let proto: Result<UnvalidatedProtoEntry, ProtoverError> = "Fribble".parse(); + + assert_eq!(Err(ProtoverError::Unparseable), proto); +} + +#[test] +fn protover_all_supported_over_maximum_limit() { + let proto: Result<UnvalidatedProtoEntry, ProtoverError> = "Sleen=0-4294967295".parse(); + + assert_eq!(Err(ProtoverError::ExceedsMax), proto); +} diff --git a/src/rust/smartlist/Cargo.toml b/src/rust/smartlist/Cargo.toml new file mode 100644 index 0000000000..6ddcbee8e9 --- /dev/null +++ b/src/rust/smartlist/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "smartlist" + +[dependencies] +libc = "0.2.39" + +[lib] +name = "smartlist" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/smartlist/lib.rs b/src/rust/smartlist/lib.rs new file mode 100644 index 0000000000..14a8148315 --- /dev/null +++ b/src/rust/smartlist/lib.rs @@ -0,0 +1,8 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +extern crate libc; + +mod smartlist; + +pub use smartlist::*; diff --git a/src/rust/smartlist/smartlist.rs b/src/rust/smartlist/smartlist.rs new file mode 100644 index 0000000000..2a822d89f4 --- /dev/null +++ b/src/rust/smartlist/smartlist.rs @@ -0,0 +1,115 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +use std::slice; +use libc::{c_char, c_int}; +use std::ffi::CStr; + +/// Smartlists are a type used in C code in tor to define a collection of a +/// generic type, which has a capacity and a number used. Each Smartlist +/// defines how to extract the list of values from the underlying C structure +/// +/// Implementations are required to have a C representation, as this module +/// serves purely to translate smartlists as defined in tor to vectors in Rust. +pub trait Smartlist<T> { + fn get_list(&self) -> Vec<T>; +} + +#[repr(C)] +pub struct Stringlist { + pub list: *const *const c_char, + pub num_used: c_int, + pub capacity: c_int, +} + +impl Smartlist<String> for Stringlist { + fn get_list(&self) -> Vec<String> { + let empty: Vec<String> = Vec::new(); + let mut rust_list: Vec<String> = Vec::new(); + + if self.list.is_null() || self.num_used == 0 { + return empty; + } + + // unsafe, as we need to extract the smartlist list into a vector of + // pointers, and then transform each element into a Rust string. + let elems: &[*const c_char] = + unsafe { slice::from_raw_parts(self.list, self.num_used as usize) }; + + for elem in elems.iter() { + if elem.is_null() { + continue; + } + + // unsafe, as we need to create a cstring from the referenced + // element + let c_string = unsafe { CStr::from_ptr(*elem) }; + + let r_string = match c_string.to_str() { + Ok(n) => n, + Err(_) => return empty, + }; + + rust_list.push(String::from(r_string)); + } + + rust_list + } +} + +// TODO: CHK: this module maybe should be tested from a test in C with a +// smartlist as defined in tor. +#[cfg(test)] +mod test { + #[test] + fn test_get_list_of_strings() { + extern crate libc; + + use std::ffi::CString; + use libc::c_char; + + use super::Smartlist; + use super::Stringlist; + + { + // test to verify that null pointers are gracefully handled + use std::ptr; + + let sl = Stringlist { + list: ptr::null(), + num_used: 0, + capacity: 0, + }; + + let data = sl.get_list(); + assert_eq!(0, data.len()); + } + + { + let args = vec![String::from("a"), String::from("b")]; + + // for each string, transform it into a CString + let c_strings: Vec<_> = args.iter() + .map(|arg| CString::new(arg.as_str()).unwrap()) + .collect(); + + // then, collect a pointer for each CString + let p_args: Vec<_> = + c_strings.iter().map(|arg| arg.as_ptr()).collect(); + + let p: *const *const c_char = p_args.as_ptr(); + + // This is the representation that we expect when receiving a + // smartlist at the Rust/C FFI layer. + let sl = Stringlist { + list: p, + num_used: 2, + capacity: 2, + }; + + let data = sl.get_list(); + assert_eq!("a", &data[0]); + assert_eq!("b", &data[1]); + } + } +} diff --git a/src/rust/tor_allocate/Cargo.toml b/src/rust/tor_allocate/Cargo.toml new file mode 100644 index 0000000000..468425f115 --- /dev/null +++ b/src/rust/tor_allocate/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "tor_allocate" + +[dependencies] +libc = "=0.2.39" + +[lib] +name = "tor_allocate" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/tor_allocate/lib.rs b/src/rust/tor_allocate/lib.rs new file mode 100644 index 0000000000..937a5dcf63 --- /dev/null +++ b/src/rust/tor_allocate/lib.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! Allocation helper functions that allow data to be allocated in Rust +//! using tor's specified allocator. In doing so, this can be later freed +//! from C. +//! +//! This is currently a temporary solution, we will later use tor's allocator +//! by default for any allocation that occurs in Rust. However, as this will +//! stabalize in 2018, we can use this as a temporary measure. + +extern crate libc; + +mod tor_allocate; +pub use tor_allocate::*; diff --git a/src/rust/tor_allocate/tor_allocate.rs b/src/rust/tor_allocate/tor_allocate.rs new file mode 100644 index 0000000000..359df1cd7a --- /dev/null +++ b/src/rust/tor_allocate/tor_allocate.rs @@ -0,0 +1,97 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +use libc::{c_char, c_void}; +use std::{ptr, slice, mem}; + +#[cfg(not(test))] +extern "C" { + fn tor_malloc_(size: usize) -> *mut c_void; +} + +// Defined only for tests, used for testing purposes, so that we don't need +// to link to tor C files. Uses the system allocator +#[cfg(test)] +unsafe extern "C" fn tor_malloc_(size: usize) -> *mut c_void { + use libc::malloc; + malloc(size) +} + +/// Allocate memory using tor_malloc_ and copy an existing string into the +/// allocated buffer, returning a pointer that can later be called in C. +/// +/// # Inputs +/// +/// * `src`, a reference to a String. +/// +/// # Returns +/// +/// A `*mut c_char` that should be freed by tor_free in C +/// +pub fn allocate_and_copy_string(src: &String) -> *mut c_char { + let bytes: &[u8] = src.as_bytes(); + + let size = mem::size_of_val::<[u8]>(bytes); + let size_one_byte = mem::size_of::<u8>(); + + // handle integer overflow when adding one to the calculated length + let size_with_null_byte = match size.checked_add(size_one_byte) { + Some(n) => n, + None => return ptr::null_mut(), + }; + + let dest = unsafe { tor_malloc_(size_with_null_byte) as *mut u8 }; + + if dest.is_null() { + return ptr::null_mut(); + } + + unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), dest, size) }; + + // set the last byte as null, using the ability to index into a slice + // rather than doing pointer arithmatic + let slice = unsafe { slice::from_raw_parts_mut(dest, size_with_null_byte) }; + slice[size] = 0; // add a null terminator + + dest as *mut c_char +} + +#[cfg(test)] +mod test { + + #[test] + fn test_allocate_and_copy_string_with_empty() { + use std::ffi::CStr; + use libc::{free, c_void}; + + use tor_allocate::allocate_and_copy_string; + + let empty = String::new(); + let allocated_empty = allocate_and_copy_string(&empty); + + let allocated_empty_rust = + unsafe { CStr::from_ptr(allocated_empty).to_str().unwrap() }; + + assert_eq!("", allocated_empty_rust); + + unsafe { free(allocated_empty as *mut c_void) }; + } + + #[test] + fn test_allocate_and_copy_string_with_not_empty_string() { + use std::ffi::CStr; + use libc::{free, c_void}; + + use tor_allocate::allocate_and_copy_string; + + let empty = String::from("foo bar biz"); + let allocated_empty = allocate_and_copy_string(&empty); + + let allocated_empty_rust = + unsafe { CStr::from_ptr(allocated_empty).to_str().unwrap() }; + + assert_eq!("foo bar biz", allocated_empty_rust); + + unsafe { free(allocated_empty as *mut c_void) }; + } +} diff --git a/src/rust/tor_rust/Cargo.toml b/src/rust/tor_rust/Cargo.toml new file mode 100644 index 0000000000..86fad3ee76 --- /dev/null +++ b/src/rust/tor_rust/Cargo.toml @@ -0,0 +1,16 @@ +[package] +authors = ["The Tor Project"] +name = "tor_rust" +version = "0.1.0" + +[lib] +name = "tor_rust" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + +[dependencies.tor_util] +path = "../tor_util" + +[dependencies.protover] +path = "../protover" + diff --git a/src/rust/tor_rust/include.am b/src/rust/tor_rust/include.am new file mode 100644 index 0000000000..c02324cb77 --- /dev/null +++ b/src/rust/tor_rust/include.am @@ -0,0 +1,30 @@ +EXTRA_DIST +=\ + src/rust/tor_rust/Cargo.toml \ + src/rust/tor_rust/lib.rs + +EXTRA_CARGO_OPTIONS= + +src/rust/target/release/@TOR_RUST_STATIC_NAME@: FORCE + ( cd "$(abs_top_builddir)/src/rust" ; \ + CARGO_TARGET_DIR="$(abs_top_builddir)/src/rust/target" \ + CARGO_HOME="$(abs_top_builddir)/src/rust" \ + $(CARGO) build --release $(EXTRA_CARGO_OPTIONS) \ + $(CARGO_ONLINE) \ + --manifest-path "$(abs_top_srcdir)/src/rust/tor_rust/Cargo.toml" ) + +distclean-rust: + ( cd "$(abs_top_builddir)/src/rust" ; \ + CARGO_TARGET_DIR="$(abs_top_builddir)/src/rust/target" \ + CARGO_HOME="$(abs_top_builddir)/src/rust" \ + $(CARGO) clean $(EXTRA_CARGO_OPTIONS) \ + $(CARGO_ONLINE) \ + --manifest-path "$(abs_top_srcdir)/src/rust/tor_rust/Cargo.toml" ) + rm -rf "$(abs_top_builddir)/src/rust/registry" + +if USE_RUST +build-rust: src/rust/target/release/@TOR_RUST_STATIC_NAME@ +else +build-rust: +endif + +FORCE: diff --git a/src/rust/tor_rust/lib.rs b/src/rust/tor_rust/lib.rs new file mode 100644 index 0000000000..c1585c0480 --- /dev/null +++ b/src/rust/tor_rust/lib.rs @@ -0,0 +1,5 @@ +extern crate tor_util; +extern crate protover; + +pub use tor_util::*; +pub use protover::*; diff --git a/src/rust/tor_util/Cargo.toml b/src/rust/tor_util/Cargo.toml index f175fbdfb0..b540d8c847 100644 --- a/src/rust/tor_util/Cargo.toml +++ b/src/rust/tor_util/Cargo.toml @@ -8,6 +8,9 @@ name = "tor_util" path = "lib.rs" crate_type = ["rlib", "staticlib"] +[dependencies.tor_allocate] +path = "../tor_allocate" + [dependencies] -libc = "*" +libc = "=0.2.39" diff --git a/src/rust/tor_util/ffi.rs b/src/rust/tor_util/ffi.rs index af4bfc41af..5c3cdba4be 100644 --- a/src/rust/tor_util/ffi.rs +++ b/src/rust/tor_util/ffi.rs @@ -1,56 +1,26 @@ -//! FFI functions, only to be called from C. -//! -//! Equivalent C versions of these live in `src/common/compat_rust.c` - -use std::mem::forget; -use std::ffi::CString; +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ -use libc; -use rust_string::RustString; - -/// Free the passed `RustString` (`rust_str_t` in C), to be used in place of -/// `tor_free`(). -/// -/// # Examples -/// ```c -/// rust_str_t r_s = rust_welcome_string(); -/// rust_str_free(r_s); -/// ``` -#[no_mangle] -#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] -pub unsafe extern "C" fn rust_str_free(_str: RustString) { - // Empty body: Just drop _str and we're done (Drop takes care of it). -} +//! FFI functions to announce Rust support during tor startup, only to be +//! called from C. +//! -/// Lends an immutable, NUL-terminated C String. -/// -/// # Examples -/// ```c -/// rust_str_t r_s = rust_welcome_string(); -/// const char *s = rust_str_get(r_s); -/// printf("%s", s); -/// rust_str_free(r_s); -/// ``` -#[no_mangle] -pub unsafe extern "C" fn rust_str_get(str: RustString) -> *const libc::c_char { - let res = str.as_ptr(); - forget(str); - res -} +use libc::c_char; +use tor_allocate::allocate_and_copy_string; /// Returns a short string to announce Rust support during startup. /// /// # Examples /// ```c -/// rust_str_t r_s = rust_welcome_string(); -/// const char *s = rust_str_get(r_s); -/// printf("%s", s); -/// rust_str_free(r_s); +/// char *rust_str = rust_welcome_string(); +/// printf("%s", rust_str); +/// tor_free(rust_str); /// ``` #[no_mangle] -pub extern "C" fn rust_welcome_string() -> RustString { - let s = CString::new("Tor is running with Rust integration. Please report \ - any bugs you encouter.") - .unwrap(); - RustString::from(s) +pub extern "C" fn rust_welcome_string() -> *mut c_char { + let rust_welcome = String::from( + "Tor is running with Rust integration. Please report \ + any bugs you encounter.", + ); + allocate_and_copy_string(&rust_welcome) } diff --git a/src/rust/tor_util/include.am b/src/rust/tor_util/include.am deleted file mode 100644 index ec3898577b..0000000000 --- a/src/rust/tor_util/include.am +++ /dev/null @@ -1,13 +0,0 @@ -EXTRA_DIST +=\ - src/rust/tor_util/Cargo.toml \ - src/rust/tor_util/lib.rs \ - src/rust/tor_util/ffi.rs \ - src/rust/tor_util/rust_string.rs - -src/rust/target/release/@TOR_RUST_UTIL_STATIC_NAME@: FORCE - ( cd "$(abs_top_srcdir)/src/rust/tor_util" ; \ - CARGO_TARGET_DIR="$(abs_top_builddir)/src/rust/target" \ - CARGO_HOME="$(abs_top_builddir)/src/rust" \ - $(CARGO) build --release --quiet $(CARGO_ONLINE) ) - -FORCE: diff --git a/src/rust/tor_util/lib.rs b/src/rust/tor_util/lib.rs index 79d583d1ae..12cb3896b6 100644 --- a/src/rust/tor_util/lib.rs +++ b/src/rust/tor_util/lib.rs @@ -1,13 +1,11 @@ -//! C <-> Rust compatibility helpers and types. -//! -//! Generically useful, small scale helpers should go here. This goes for both -//! the C side (in the form of the ffi module) as well as the Rust side -//! (individual modules per functionality). The corresponding C stuff lives in -//! `src/common/compat_rust.{c,h}`. +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! Small module to announce Rust support during startup for demonstration +//! purposes. extern crate libc; +extern crate tor_allocate; -mod rust_string; pub mod ffi; - -pub use rust_string::*; +pub mod strings; diff --git a/src/rust/tor_util/rust_string.rs b/src/rust/tor_util/rust_string.rs deleted file mode 100644 index 46ec3fd7a8..0000000000 --- a/src/rust/tor_util/rust_string.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::ffi::CString; -use std::mem::forget; -use libc; - -/// Compatibility wrapper for strings allocated in Rust and passed to C. -/// -/// Rust doesn't ensure the safety of freeing memory across an FFI boundary, so -/// we need to take special care to ensure we're not accidentally calling -/// `tor_free`() on any string allocated in Rust. To more easily differentiate -/// between strings that possibly (if Rust support is enabled) were allocated -/// in Rust, C has the `rust_str_t` helper type. The equivalent on the Rust -/// side is `RustString`. -/// -/// Note: This type must not be used for strings allocated in C. -#[repr(C)] -#[derive(Debug)] -pub struct RustString(*mut libc::c_char); - -impl RustString { - /// Returns a pointer to the underlying NUL-terminated byte array. - /// - /// Note that this function is not typically useful for Rust callers, - /// except in a direct FFI context. - /// - /// # Examples - /// ``` - /// # use tor_util::RustString; - /// use std::ffi::CString; - /// - /// let r = RustString::from(CString::new("asdf").unwrap()); - /// let c_str = r.as_ptr(); - /// assert_eq!(b'a', unsafe { *c_str as u8}); - /// ``` - pub fn as_ptr(&self) -> *const libc::c_char { - self.0 as *const libc::c_char - } -} - -impl From<CString> for RustString { - /// Constructs a new `RustString` - /// - /// # Examples - /// ``` - /// # use tor_util::RustString; - /// use std::ffi::CString; - /// - /// let r = RustString::from(CString::new("asdf").unwrap()); - /// ``` - fn from(str: CString) -> RustString { - RustString(str.into_raw()) - } -} - -impl Into<CString> for RustString { - /// Reconstructs a `CString` from this `RustString`. - /// - /// Useful to take ownership back from a `RustString` that was given to C - /// code. - /// - /// # Examples - /// ``` - /// # use tor_util::RustString; - /// use std::ffi::CString; - /// - /// let cs = CString::new("asdf").unwrap(); - /// let r = RustString::from(cs.clone()); - /// let cs2 = r.into(); - /// assert_eq!(cs, cs2); - /// ``` - fn into(self) -> CString { - // Calling from_raw is always OK here: We only construct self using - // valid CStrings and don't expose anything that could mutate it - let ret = unsafe { CString::from_raw(self.0) }; - forget(self); - ret - } -} - -impl Drop for RustString { - fn drop(&mut self) { - // Don't use into() here, because we would need to move out of - // self. Same safety consideration. Immediately drop the created - // CString, which takes care of freeing the wrapped string. - unsafe { CString::from_raw(self.0) }; - } -} - -#[cfg(test)] -mod test { - use std::mem; - use super::*; - - use libc; - - /// Ensures we're not adding overhead by using RustString. - #[test] - fn size_of() { - assert_eq!(mem::size_of::<*mut libc::c_char>(), - mem::size_of::<RustString>()) - } -} diff --git a/src/rust/tor_util/strings.rs b/src/rust/tor_util/strings.rs new file mode 100644 index 0000000000..9321ce4f85 --- /dev/null +++ b/src/rust/tor_util/strings.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2016-2017, The Tor Project, Inc. */ +// See LICENSE for licensing information */ + +//! Utilities for working with static strings. + +use std::ffi::CStr; + +/// A byte-array containing a single NUL byte (`b"\0"`). +pub const NUL_BYTE: &'static [u8] = b"\0"; + +/// Determine if a byte slice is a C-like string. +/// +/// These checks guarantee that: +/// +/// 1. there are no intermediate NUL bytes +/// 2. the last byte *is* a NUL byte +/// +/// # Warning +/// +/// This function does _not_ guarantee that the bytes represent any valid +/// encoding such as ASCII or UTF-8. +/// +/// # Examples +/// +/// ``` +/// # use tor_util::strings::byte_slice_is_c_like; +/// # +/// let bytes: &[u8] = b"foo bar baz"; +/// +/// assert!(byte_slice_is_c_like(&bytes) == false); +/// +/// let bytes: &[u8] = b"foo\0bar baz"; +/// +/// assert!(byte_slice_is_c_like(&bytes) == false); +/// +/// let bytes: &[u8] = b"foo bar baz\0"; +/// +/// assert!(byte_slice_is_c_like(&bytes) == true); +/// ``` +pub fn byte_slice_is_c_like(bytes: &[u8]) -> bool { + if !bytes[..bytes.len() - 1].contains(&0x00) && bytes[bytes.len() - 1] == 0x00 { + return true; + } + false +} + +/// Get a static `CStr` containing a single `NUL_BYTE`. +/// +/// # Examples +/// +/// When used as follows in a Rust FFI function, which could be called +/// from C: +/// +/// ``` +/// # extern crate libc; +/// # extern crate tor_util; +/// # +/// # use tor_util::strings::empty_static_cstr; +/// use libc::c_char; +/// use std::ffi::CStr; +/// +/// pub extern "C" fn give_c_code_an_empty_static_string() -> *const c_char { +/// let empty: &'static CStr = empty_static_cstr(); +/// +/// empty.as_ptr() +/// } +/// +/// # fn main() { +/// # give_c_code_an_empty_static_string(); +/// # } +/// ``` +/// +/// This equates to an "empty" `const char*` static string in C. +pub fn empty_static_cstr() -> &'static CStr { + let empty: &'static CStr; + + unsafe { + empty = CStr::from_bytes_with_nul_unchecked(NUL_BYTE); + } + + empty +} diff --git a/src/rust/tor_util/tests/rust_string.rs b/src/rust/tor_util/tests/rust_string.rs deleted file mode 100644 index 1ff605a43c..0000000000 --- a/src/rust/tor_util/tests/rust_string.rs +++ /dev/null @@ -1,37 +0,0 @@ -extern crate tor_util; -extern crate libc; - -use std::ffi::CString; -use tor_util::RustString; - -#[test] -fn rust_string_conversions_preserve_c_string() { - let s = CString::new("asdf foo").unwrap(); - let r = RustString::from(s.clone()); - let r2 = RustString::from(s.clone()); - let c = r2.as_ptr(); - assert_eq!(unsafe { libc::strlen(c) }, 8); - let c_str = r.into(); - assert_eq!(s, c_str); -} - -#[test] -fn empty_string() { - let s = CString::new("").unwrap(); - let r = RustString::from(s.clone()); - let c = r.as_ptr(); - assert_eq!(unsafe { libc::strlen(c) }, 0); - let c_str = r.into(); - assert_eq!(s, c_str); -} - -#[test] -fn c_string_with_unicode() { - // The euro sign is three bytes - let s = CString::new("asd€asd").unwrap(); - let r = RustString::from(s.clone()); - let c = r.as_ptr(); - assert_eq!(unsafe { libc::strlen(c) }, 9); - let c_str = r.into(); - assert_eq!(s, c_str); -} |