diff --git a/Makefile.am b/Makefile.am index ad2ceb66a9..9067e9a8a4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -27,6 +27,8 @@ endif if USE_RUST rust_ldadd=$(top_builddir)/src/rust/target/release/@TOR_RUST_UTIL_STATIC_NAME@ +rust_ldadd+=$(top_builddir)/src/rust/target/release/@TOR_RUST_PROTOVER_STATIC_NAME@ +rust_ldadd+=$(top_builddir)/src/rust/target/release/@TOR_RUST_C_STRING_STATIC_NAME@ else rust_ldadd= endif diff --git a/configure.ac b/configure.ac index d2682944fb..b2b353f6ba 100644 --- a/configure.ac +++ b/configure.ac @@ -440,11 +440,17 @@ if test "x$enable_rust" = "xyes"; then dnl the MSVC naming convention. if test "$bwin32" = "true"; then TOR_RUST_UTIL_STATIC_NAME=tor_util.lib + TOR_RUST_PROTOVER_STATIC_NAME=libprotover.lib + TOR_RUST_C_STRING_STATIC_NAME=libc_string.lib else TOR_RUST_UTIL_STATIC_NAME=libtor_util.a + TOR_RUST_PROTOVER_STATIC_NAME=libprotover.a + TOR_RUST_C_STRING_STATIC_NAME=libc_string.a fi AC_SUBST(TOR_RUST_UTIL_STATIC_NAME) + AC_SUBST(TOR_RUST_PROTOVER_STATIC_NAME) + AC_SUBST(TOR_RUST_C_STRING_STATIC_NAME) AC_SUBST(CARGO_ONLINE) AC_SUBST(RUST_DL) diff --git a/src/common/include.am b/src/common/include.am index cd5eea3404..7ce84e17c5 100644 --- a/src/common/include.am +++ b/src/common/include.am @@ -94,6 +94,7 @@ LIBOR_A_SRC = \ src/common/util_bug.c \ src/common/util_format.c \ src/common/util_process.c \ + src/common/rust_types.c \ src/common/sandbox.c \ src/common/storagedir.c \ src/common/workqueue.c \ @@ -179,6 +180,7 @@ COMMONHEADERS = \ src/common/procmon.h \ src/common/pubsub.h \ src/common/sandbox.h \ + src/common/rust_types.h \ src/common/storagedir.h \ src/common/testsupport.h \ src/common/timers.h \ diff --git a/src/common/rust_types.c b/src/common/rust_types.c new file mode 100644 index 0000000000..d116b515b8 --- /dev/null +++ b/src/common/rust_types.c @@ -0,0 +1,55 @@ +/* Copyright (c) 2017, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * \file rust_types.c + * \brief This file is used for handling types returned from Rust to C. + **/ + +#include "or.h" +#include "rust_types.h" + +#ifdef HAVE_RUST + +void free_rust_str(char *ret); + +/* Because Rust strings can only be freed from Rust, we first copy the string's + * contents to a c pointer, and then free the Rust string. + * This function can be extended to return a success/error value if needed. + */ +void +move_rust_str_to_c_and_free(rust_str_ref_t src, char **dest) +{ + if (!src) { + log_warn(LD_BUG, "Received a null pointer from protover rust."); + return; + } + + if (!dest) { + log_warn(LD_BUG, "Received a null pointer from caller to protover rust. " + "This results in a memory leak due to not freeing the rust " + "string that was meant to be copied.."); + return; + } + + *dest = tor_strdup(src); + free_rust_str(src); + return; +} + +#else + +/* When Rust is not enabled, this function should never be used. Log a warning + * in the case that it is ever called when Rust is not enabled. + */ +void +move_rust_str_to_c_and_free(rust_str_ref_t src, char **dest) +{ + (void) src; + (void) dest; + log_warn(LD_BUG, "Received a call to free a Rust string when we are " + " not running with Rust enabled."); + return; +} +#endif /* defined(HAVE_RUST) */ + diff --git a/src/common/rust_types.h b/src/common/rust_types.h new file mode 100644 index 0000000000..b6d807e656 --- /dev/null +++ b/src/common/rust_types.h @@ -0,0 +1,22 @@ +/* Copyright (c) 2017, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/** + * \file rust_types.h + * \brief Headers for rust_types.c + **/ + +#include "or.h" + +#ifndef TOR_RUST_TYPES_H +#define TOR_RUST_TYPES_H + +/* This type is used to clearly mark strings that have been allocated in Rust, + * and therefore strictly need to use the free_rust_str method to free. + */ +typedef char *rust_str_ref_t; + +void move_rust_str_to_c_and_free(rust_str_ref_t src, char **dest); + +#endif + diff --git a/src/or/include.am b/src/or/include.am index 7216aba9af..bf3715e95e 100644 --- a/src/or/include.am +++ b/src/or/include.am @@ -78,6 +78,7 @@ LIBTOR_A_SOURCES = \ src/or/parsecommon.c \ src/or/periodic.c \ src/or/protover.c \ + src/or/protover_rust.c \ src/or/proto_cell.c \ src/or/proto_control0.c \ src/or/proto_ext_or.c \ diff --git a/src/or/protover.c b/src/or/protover.c index 1a3e69be10..0e74deb112 100644 --- a/src/or/protover.c +++ b/src/or/protover.c @@ -27,6 +27,8 @@ #include "protover.h" #include "routerparse.h" +#ifndef HAVE_RUST + static const smartlist_t *get_supported_protocol_list(void); static int protocol_list_contains(const smartlist_t *protos, protocol_type_t pr, uint32_t ver); @@ -735,3 +737,5 @@ protover_free_all(void) } } +#endif + diff --git a/src/or/protover.h b/src/or/protover.h index 657977279e..7f1938e0c9 100644 --- a/src/or/protover.h +++ b/src/or/protover.h @@ -70,11 +70,15 @@ typedef struct proto_entry_t { smartlist_t *ranges; } proto_entry_t; +#if !defined(HAVE_RUST) && defined(TOR_UNIT_TESTS) STATIC smartlist_t *parse_protocol_list(const char *s); -STATIC void proto_entry_free(proto_entry_t *entry); STATIC char *encode_protocol_list(const smartlist_t *sl); STATIC const char *protocol_type_to_str(protocol_type_t pr); STATIC int str_to_protocol_type(const char *s, protocol_type_t *pr_out); +STATIC void proto_entry_free(proto_entry_t *entry); + +#endif + #endif /* defined(PROTOVER_PRIVATE) */ #endif /* !defined(TOR_PROTOVER_H) */ diff --git a/src/or/protover_rust.c b/src/or/protover_rust.c new file mode 100644 index 0000000000..ebe815357b --- /dev/null +++ b/src/or/protover_rust.c @@ -0,0 +1,111 @@ +/* Copyright (c) 2016-2017, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +/* + * \file protover_rust.c + * \brief Provide a C wrapper for functions exposed in /src/rust/protover, + * and safe translation/handling between the Rust/C boundary. + */ + +#include "or.h" +#include "protover.h" +#include "rust_types.h" + +#ifdef HAVE_RUST + +int rust_protover_all_supported(const char *s, char **missing); +rust_str_ref_t rust_protover_compute_for_old_tor(const char *version); +rust_str_ref_t rust_protover_compute_vote(const smartlist_t *proto_votes, + int threshold); +rust_str_ref_t rust_protover_get_supported_protocols(void); +int rust_protocol_list_supports_protocol(const char *list, protocol_type_t tp, + uint32_t version); +int rust_protover_is_supported_here(protocol_type_t pr, uint32_t ver); + +/* Define for compatibility, used in main.c */ +void protover_free_all(void) {}; + +/* + * Wrap rust_protover_is_supported_here, located in /src/rust/protover + */ +int +protover_is_supported_here(protocol_type_t pr, uint32_t ver) +{ + return rust_protover_is_supported_here(pr, ver); +} + +/* + * Wrap rust_protover_list_supports_protocol, located in /src/rust/protover + */ +int +protocol_list_supports_protocol(const char *list, protocol_type_t tp, + uint32_t version) +{ + return rust_protocol_list_supports_protocol(list, tp, version); +} + +/* + * Wrap rust_protover_get_supported_protocols, located in /src/rust/protover + */ +const char * +protover_get_supported_protocols(void) +{ + rust_str_ref_t rust_protocols = rust_protover_get_supported_protocols(); + + char *protocols = NULL; + if (rust_protocols != NULL) { + move_rust_str_to_c_and_free(rust_protocols, &protocols); + } + return protocols; +} + +/* + * Wrap rust_protover_compute_vote, located in /src/rust/protover + */ +char * +protover_compute_vote(const smartlist_t *proto_strings, + int threshold) +{ + rust_str_ref_t rust_protocols = rust_protover_compute_vote(proto_strings, + threshold); + + char *protocols = NULL; + if (rust_protocols != NULL) { + move_rust_str_to_c_and_free(rust_protocols, &protocols); + } + return protocols; +} + +/* + * Wrap rust_protover_all_supported, located in /src/rust/protover + */ +int +protover_all_supported(const char *s, char **missing_out) +{ + rust_str_ref_t missing_out_copy = NULL; + int is_supported = rust_protover_all_supported(s, &missing_out_copy); + + if (!is_supported) { + move_rust_str_to_c_and_free(missing_out_copy, missing_out); + } + + return is_supported; +} + +/* + * Wrap rust_compute_for_old_tor, located in /src/rust/protover + */ +const char * +protover_compute_for_old_tor(const char *version) +{ + rust_str_ref_t rust_protocols = rust_protover_compute_for_old_tor(version); + + char *protocols = NULL; + if (rust_protocols != NULL) { + move_rust_str_to_c_and_free(rust_protocols, &protocols); + } + return protocols; +} + +#endif + diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 4ac9606ce8..aa91ea355c 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -5,10 +5,41 @@ dependencies = [ "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "c_string" +version = "0.0.1" +dependencies = [ + "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "external" +version = "0.0.1" +dependencies = [ + "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "libc" version = "0.2.22" 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.22 (registry+https://github.com/rust-lang/crates.io-index)", + "smartlist 0.0.1", + "tor_util 0.0.1", +] + +[[package]] +name = "smartlist" +version = "0.0.1" +dependencies = [ + "libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index fc4377e8b4..6943627e38 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", "c_string"] [profile.release] debug = true diff --git a/src/rust/c_string/Cargo.toml b/src/rust/c_string/Cargo.toml new file mode 100644 index 0000000000..dc7504856b --- /dev/null +++ b/src/rust/c_string/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "c_string" + +[dependencies] +libc = "0.2.22" + +[lib] +name = "c_string" +path = "ffi.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/c_string/ffi.rs b/src/rust/c_string/ffi.rs new file mode 100644 index 0000000000..edce250829 --- /dev/null +++ b/src/rust/c_string/ffi.rs @@ -0,0 +1,19 @@ +//! FFI functions, only to be called from C. +//! +//! This module provides the ability for C to free strings that have been +//! allocated in Rust. + +extern crate libc; + +use libc::c_char; +use std::ffi::CString; + +/// This allows strings allocated in Rust to be freed in Rust. Every string +/// sent across the Rust/C FFI boundary should utilize this function for +/// freeing strings allocated in Rust. +#[no_mangle] +pub extern "C" fn free_rust_str(ptr: *mut c_char) { + if !ptr.is_null() { + unsafe { CString::from_raw(ptr) }; + } +} diff --git a/src/rust/c_string/include.am b/src/rust/c_string/include.am new file mode 100644 index 0000000000..8e9229ae6a --- /dev/null +++ b/src/rust/c_string/include.am @@ -0,0 +1,12 @@ +EXTRA_DIST +=\ + src/rust/c_string/Cargo.toml \ + src/rust/c_string/ffi.rs + +src/rust/target/release/@TOR_RUST_C_STRING_STATIC_NAME@: FORCE + ( cd "$(abs_top_srcdir)/src/rust/c_string" ; \ + 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/external/Cargo.toml b/src/rust/external/Cargo.toml new file mode 100644 index 0000000000..bccd7033a7 --- /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.22" + +[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..0e8d1eb0d8 --- /dev/null +++ b/src/rust/external/external.rs @@ -0,0 +1,29 @@ +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 { + result = 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..cba92c28aa 100644 --- a/src/rust/include.am +++ b/src/rust/include.am @@ -1,4 +1,6 @@ include src/rust/tor_util/include.am +include src/rust/protover/include.am +include src/rust/c_string/include.am EXTRA_DIST +=\ src/rust/Cargo.toml \ diff --git a/src/rust/protover/Cargo.toml b/src/rust/protover/Cargo.toml new file mode 100644 index 0000000000..a8f794f838 --- /dev/null +++ b/src/rust/protover/Cargo.toml @@ -0,0 +1,22 @@ +[package] +authors = ["The Tor Project"] +version = "0.0.1" +name = "protover" + +[dependencies] +libc = "0.2.22" + +[dependencies.smartlist] +path = "../smartlist" + +[dependencies.external] +path = "../external" + +[dependencies.tor_util] +path = "../tor_util" + +[lib] +name = "protover" +path = "lib.rs" +crate_type = ["rlib", "staticlib"] + diff --git a/src/rust/protover/ffi.rs b/src/rust/protover/ffi.rs new file mode 100644 index 0000000000..7365d7cd8f --- /dev/null +++ b/src/rust/protover/ffi.rs @@ -0,0 +1,205 @@ +//! 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 protover::*; +use smartlist::*; + +/// Translate C enums to Rust Proto enums, using the integer value of the C +/// enum to map to its associated Rust enum +/// This is dependant on the associated C enum preserving ordering. +/// Modify the C documentation to give warnings- you must also re-order the rust +fn translate_to_rust(c_proto: uint32_t) -> Result { + match c_proto { + 0 => Ok(Proto::Link), + 1 => Ok(Proto::LinkAuth), + 2 => Ok(Proto::Relay), + 3 => Ok(Proto::DirCache), + 4 => Ok(Proto::HSDir), + 5 => Ok(Proto::HSIntro), + 6 => Ok(Proto::HSRend), + 7 => Ok(Proto::Desc), + 8 => Ok(Proto::Microdesc), + 9 => Ok(Proto::Cons), + _ => Err("Invalid protocol type"), + } +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::all_supported +#[no_mangle] +pub extern "C" fn rust_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 { + c_str = CStr::from_ptr(c_relay_version); + } + + let relay_version = match c_str.to_str() { + Ok(n) => n, + Err(_) => return 1, + }; + + let (is_supported, unsupported) = all_supported(relay_version); + + if unsupported.len() > 0 { + let c_unsupported = match CString::new(unsupported) { + Ok(n) => n, + Err(_) => return 1, + }; + + let ptr = c_unsupported.into_raw(); + unsafe { *missing_out = ptr }; + } + + return if is_supported { 1 } else { 0 }; +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::list_supports_protocol +#[no_mangle] +pub extern "C" fn rust_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 { + c_str = 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 is_supported = + protover_string_supports_protocol(protocol_list, protocol, version); + + return if is_supported { 1 } else { 0 }; +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::get_supported_protocols +#[no_mangle] +pub extern "C" fn rust_protover_get_supported_protocols() -> *mut c_char { + // Not handling errors when unwrapping as the content is controlled + // and is an empty string + let empty = CString::new("").unwrap(); + + let supported = get_supported_protocols(); + let c_supported = match CString::new(supported) { + Ok(n) => n, + Err(_) => return empty.into_raw(), + }; + + c_supported.into_raw() +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::compute_vote +#[no_mangle] +pub extern "C" fn rust_protover_compute_vote( + list: *const Stringlist, + threshold: c_int, +) -> *mut c_char { + // Not handling errors when unwrapping as the content is controlled + // and is an empty string + let empty = CString::new("").unwrap(); + + if list.is_null() { + return empty.into_raw(); + } + + // Dereference of raw pointer requires an unsafe block. The pointer is + // checked above to ensure it is not null. + let data: Vec; + unsafe { + data = (*list).get_list(); + } + + let vote = compute_vote(data, threshold); + let c_vote = match CString::new(vote) { + Ok(n) => n, + Err(_) => return empty.into_raw(), + }; + + c_vote.into_raw() +} + +/// Provide an interface for C to translate arguments and return types for +/// protover::is_supported_here +#[no_mangle] +pub extern "C" fn rust_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 rust_protover_compute_for_old_tor( + version: *const c_char, +) -> *mut c_char { + // Not handling errors when unwrapping as the content is controlled + // and is an empty string + let empty = CString::new("").unwrap(); + + if version.is_null() { + return empty.into_raw(); + } + + // 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 { + c_str = CStr::from_ptr(version); + } + + let version = match c_str.to_str() { + Ok(n) => n, + Err(_) => return empty.into_raw(), + }; + + let supported = compute_for_old_tor(&version); + + let c_supported = match CString::new(supported) { + Ok(n) => n, + Err(_) => return empty.into_raw(), + }; + + c_supported.into_raw() +} diff --git a/src/rust/protover/include.am b/src/rust/protover/include.am new file mode 100644 index 0000000000..326dbbb7f2 --- /dev/null +++ b/src/rust/protover/include.am @@ -0,0 +1,14 @@ +EXTRA_DIST +=\ + src/rust/protover/Cargo.toml \ + src/rust/protover/lib.rs \ + src/rust/protover/ffi.rs \ + src/rust/protover/external.rs + +src/rust/target/release/@TOR_RUST_PROTOVER_STATIC_NAME@: FORCE + ( cd "$(abs_top_srcdir)/src/rust/protover" ; \ + 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/protover/lib.rs b/src/rust/protover/lib.rs new file mode 100644 index 0000000000..89378c7b7e --- /dev/null +++ b/src/rust/protover/lib.rs @@ -0,0 +1,34 @@ +#![feature(inclusive_range_syntax)] + +//! 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. + +extern crate libc; +extern crate smartlist; +extern crate external; + +mod protover; +pub mod ffi; + +pub use protover::*; diff --git a/src/rust/protover/protover.rs b/src/rust/protover/protover.rs new file mode 100644 index 0000000000..0893362cec --- /dev/null +++ b/src/rust/protover/protover.rs @@ -0,0 +1,847 @@ +use external::c_tor_version_as_new_as; + +use std::str::FromStr; +use std::str::SplitN; +use std::fmt; +use std::collections::HashMap; +use std::collections::HashSet; +use std::string::String; + +/// The first version of Tor that included "proto" entries in its descriptors. +/// Authorities should use this to decide whether to guess proto lines. +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 +const MAX_PROTOCOLS_TO_EXPAND: u32 = 500; + +/// Currently supported protocols and their versions +const SUPPORTED_PROTOCOLS: &'static [&'static str] = &[ + "Cons=1-2", + "Desc=1-2", + "DirCache=1-2", + "HSDir=1-2", + "HSIntro=3-4", + "HSRend=1-2", + "Link=1-4", + "LinkAuth=1,3", + "Microdesc=1-2", + "Relay=1-2", +]; + +/// Known subprotocols in Tor. Indicates which subprotocol a relay supports. +#[derive(Hash, Eq, PartialEq, Debug)] +pub enum Proto { + Cons, + Desc, + DirCache, + HSDir, + HSIntro, + HSRend, + Link, + LinkAuth, + Microdesc, + Relay, +} + +impl fmt::Display for Proto { + 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. +impl FromStr for Proto { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "Cons" => Ok(Proto::Cons), + "Desc" => Ok(Proto::Desc), + "DirCache" => Ok(Proto::DirCache), + "HSDir" => Ok(Proto::HSDir), + "HSIntro" => Ok(Proto::HSIntro), + "HSRend" => Ok(Proto::HSRend), + "Link" => Ok(Proto::Link), + "LinkAuth" => Ok(Proto::LinkAuth), + "Microdesc" => Ok(Proto::Microdesc), + "Relay" => Ok(Proto::Relay), + _ => Err("Not a valid protocol type"), + } + } +} + +/// 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() -> String { + SUPPORTED_PROTOCOLS.join(" ") +} + +/// Translates a vector representation of a protocol list into a HashMap +fn parse_protocols( + protocols: &[&str], +) -> Result>, &'static str> { + let mut parsed = HashMap::new(); + + for subproto in protocols { + let (name, version) = get_proto_and_vers(subproto)?; + parsed.insert(name, version); + } + Ok(parsed) +} + +/// Translates a string representation of a protocol list to a HashMap +fn parse_protocols_from_string<'a>( + protocol_string: &'a str, +) -> Result>, &'static str> { + let protocols: &[&'a str] = + &protocol_string.split(" ").collect::>()[..]; + + parse_protocols(protocols) +} + +/// Translates supported tor versions from a string into a HashMap, which is +/// useful when looking up a specific subprotocol. +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is a `HashMap>` holding all +/// subprotocols and versions currently supported by tor. +/// +/// The returned `Result`'s `Err` value is an `&'static str` with a description +/// of the error. +/// +fn tor_supported() -> Result>, &'static str> { + parse_protocols(&SUPPORTED_PROTOCOLS) +} + +/// Get the unique version numbers supported by a subprotocol. +/// +/// # Inputs +/// +/// * `version_string`, a string comprised of "[0-9,-]" +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is a `HashSet` holding all of the unique +/// version numbers. If there were ranges in the `version_string`, then these +/// are expanded, i.e. `"1-3"` would expand to `HashSet::new([1, 2, 3])`. +/// The returned HashSet is *unordered*. +/// +/// The returned `Result`'s `Err` value is an `&'static str` with a description +/// of the error. +/// +/// # Errors +/// +/// This function will error if: +/// +/// * the `version_string` is empty or contains 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. +/// +fn get_versions(version_string: &str) -> Result, &'static str> { + if version_string.is_empty() { + return Err("version string is empty"); + } + + let mut versions = HashSet::::new(); + + for piece in version_string.split(",") { + if piece.contains("-") { + for p in expand_version_range(piece)? { + versions.insert(p); + } + } else { + versions.insert(u32::from_str(piece).or( + Err("invalid protocol entry"), + )?); + } + + if versions.len() > MAX_PROTOCOLS_TO_EXPAND as usize { + return Err("Too many versions to expand"); + } + } + Ok(versions) +} + + +/// Parse the subprotocol type and its version numbers. +/// +/// # Inputs +/// +/// * A `protocol_entry` string, comprised of a keyword, an "=" sign, and one +/// or more version numbers. +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is a tuple of `(Proto, HashSet)`, where the +/// first element is the subprotocol type (see `protover::Proto`) and the last +/// element is a(n unordered) set of unique version numbers which are supported. +/// Otherwise, the `Err` value of this `Result` is a description of the error +/// +fn get_proto_and_vers<'a>( + protocol_entry: &'a str, +) -> Result<(Proto, HashSet), &'static str> { + let mut parts: SplitN<'a, &str> = protocol_entry.splitn(2, "="); + + let proto: &str = match parts.next() { + Some(n) => n, + None => return Err("invalid protover entry"), + }; + + let vers: &str = match parts.next() { + Some(n) => n, + None => return Err("invalid protover entry"), + }; + + let versions = get_versions(vers)?; + let proto_name = proto.parse()?; + + Ok((proto_name, versions)) +} + +/// Parses a single subprotocol entry string into subprotocol and version +/// parts, and then checks whether any of those versions are unsupported. +/// Helper for protover::all_supported +/// +/// # Inputs +/// +/// Accepted data is in the string format as follows: +/// +/// "HSDir=1-1" +/// +/// # Returns +/// +/// Returns `true` if the protocol entry is well-formatted and only contains +/// versions that are also supported in tor. Otherwise, returns false +/// +fn contains_only_supported_protocols(proto_entry: &str) -> bool { + let (name, mut vers) = match get_proto_and_vers(proto_entry) { + Ok(n) => n, + Err(_) => return false, + }; + + let currently_supported: HashMap> = + match tor_supported() { + Ok(n) => n, + Err(_) => return false, + }; + + let supported_versions = match currently_supported.get(&name) { + Some(n) => n, + None => return false, + }; + + vers.retain(|x| !supported_versions.contains(x)); + vers.is_empty() +} + +/// Determine if we support every protocol a client supports, and if not, +/// determine which protocols we do not have support for. +/// +/// # Inputs +/// +/// Accepted data is in the string format as follows: +/// +/// "HSDir=1-1 LinkAuth=1-2" +/// +/// # Returns +/// +/// Return `true` if every protocol version is one that we support. +/// Otherwise, return `false`. +/// Optionally, return parameters which the client supports but which we do not +/// +/// # Examples +/// ``` +/// use protover::all_supported; +/// +/// let (is_supported, unsupported) = all_supported("Link=1"); +/// assert_eq!(true, is_supported); +/// +/// let (is_supported, unsupported) = all_supported("Link=5-6"); +/// assert_eq!(false, is_supported); +/// assert_eq!("Link=5-6", unsupported); +/// +pub fn all_supported(protocols: &str) -> (bool, String) { + let unsupported: Vec<&str> = protocols + .split_whitespace() + .filter(|v| !contains_only_supported_protocols(v)) + .collect::>(); + + (unsupported.is_empty(), unsupported.join(" ")) +} + +/// Return true iff the provided protocol list includes support for the +/// indicated protocol and version. +/// Otherwise, return false +/// +/// # Inputs +/// +/// * `list`, a string representation of a list of protocol entries. +/// * `proto`, a `Proto` to test support for +/// * `vers`, a `u32` version which we will go on to determine whether the +/// specified protocol supports. +/// +/// # Examples +/// ``` +/// use protover::*; +/// +/// let is_supported = protover_string_supports_protocol("Link=3-4 Cons=1", Proto::Cons,1); +/// assert_eq!(true, is_supported); +/// +/// let is_not_supported = protover_string_supports_protocol("Link=3-4 Cons=1", Proto::Cons,5); +/// assert_eq!(false, is_not_supported) +/// ``` +pub fn protover_string_supports_protocol( + list: &str, + proto: Proto, + vers: u32, +) -> bool { + let supported: HashMap>; + + match parse_protocols_from_string(list) { + Ok(result) => supported = result, + Err(_) => return false, + } + + let supported_versions = match supported.get(&proto) { + Some(n) => n, + None => return false, + }; + + supported_versions.contains(&vers) +} + +/// Fully expand a version range. For example, 1-3 expands to 1,2,3 +/// Helper for get_versions +/// +/// # Inputs +/// +/// `range`, a string comprised of "[0-9,-]" +/// +/// # Returns +/// +/// A `Result` whose `Ok` value a vector of unsigned integers representing the +/// expanded range of supported versions by a single protocol. +/// Otherwise, the `Err` value of this `Result` is a description of the error +/// +/// # Errors +/// +/// This function will error if: +/// +/// * the specified range is empty +/// * the version range does not contain both a valid lower and upper bound. +/// +fn expand_version_range(range: &str) -> Result, &'static str> { + if range.is_empty() { + return Err("version string empty"); + } + + let mut parts = range.split("-"); + + let lower_string: &str = parts.next().ok_or( + "cannot parse protocol range lower bound", + )?; + + let lower: u32 = u32::from_str_radix(lower_string, 10).or(Err( + "cannot parse protocol range lower bound", + ))?; + + let higher_string: &str = parts.next().ok_or( + "cannot parse protocol range upper bound", + )?; + + let higher: u32 = u32::from_str_radix(higher_string, 10).or(Err( + "cannot parse protocol range upper bound", + ))?; + + Ok((lower...higher).collect()) +} + +/// 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. +/// Helper for compute_vote +/// +/// # 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, and an `u32` of the last integer in the range. +/// +/// 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. +/// +fn find_range(list: &Vec) -> (bool, u32) { + if list.len() == 0 { + return (false, 0); + } + + let mut iterable = list.iter().peekable(); + let mut range_end: u32 = match iterable.next() { + Some(n) => *n, + None => return (false, 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; + } + + (has_range, range_end) +} + +/// Contracts a HashSet representation of supported versions into a string. +/// Helper for compute_vote +/// +/// # Inputs +/// +/// `supported_set`, a set of integers of "[0-9,-]" representing the +/// supported versions for a single protocol. +/// +/// # Returns +/// +/// A `String` representation of this set in ascending order. +/// +fn contract_protocol_list<'a>(supported_set: &'a HashSet) -> String { + let mut supported_clone = supported_set.clone(); + let mut supported: Vec = supported_clone.drain().collect(); + supported.sort(); + + let mut final_output: Vec = Vec::new(); + + while supported.len() != 0 { + let (has_range, end) = find_range(&supported); + let current = supported.remove(0); + + if has_range { + final_output.push(format!( + "{}-{}", + current.to_string(), + &end.to_string(), + )); + supported.retain(|&x| x > end); + } else { + final_output.push(current.to_string()); + } + } + + final_output.join(",") +} + +/// 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 Proto types +/// +/// # Returns +/// +/// A `Result` whose `Ok` value is a `HashSet` holding all of the unique +/// version numbers. +/// +/// The returned `Result`'s `Err` value is an `&'static str` with a description +/// of the error. +/// +/// # Errors +/// +/// This function will error if: +/// +/// * The protocol string does not follow the "protocol_name=version_list" +/// expected format +/// * If the version string is malformed. See `get_versions`. +/// +fn parse_protocols_from_string_with_no_validation<'a>( + protocol_string: &'a str, +) -> Result>, &'static str> { + let protocols: &[&'a str] = + &protocol_string.split(" ").collect::>()[..]; + + let mut parsed: HashMap> = HashMap::new(); + + for subproto in protocols { + let mut parts: SplitN<'a, &str> = subproto.splitn(2, "="); + + let name: &str = match parts.next() { + Some(n) => n, + None => return Err("invalid protover entry"), + }; + + let vers: &str = match parts.next() { + Some(n) => n, + None => return Err("invalid protover entry"), + }; + + let versions = get_versions(vers)?; + + parsed.insert(String::from(name), versions); + } + Ok(parsed) +} + +/// Protocol voting implementation. +/// +/// Given a list of strings describing protocol versions, return a new +/// string encoding all of the protocols that are listed by at +/// least threshold of the inputs. +/// +/// The string is sorted according to the following conventions: +/// - Protocols names are alphabetized +/// - Protocols are in order low to high +/// - Individual and ranges are listed together. For example, +/// "3, 5-10,13" +/// - All entries are unique +/// +/// # Examples +/// ``` +/// use protover::compute_vote; +/// +/// let protos = vec![String::from("Link=3-4"), String::from("Link=3")]; +/// let vote = compute_vote(protos, 2); +/// assert_eq!("Link=3", vote) +/// ``` +pub fn compute_vote( + list_of_proto_strings: Vec, + threshold: i32, +) -> String { + let empty = String::from(""); + + if list_of_proto_strings.is_empty() { + return empty; + } + + // all_count is a structure to represent the count of the number of + // supported versions for a specific protocol. For example, in JSON format: + // { + // "FirstSupportedProtocol": { + // "1": "3", + // "2": "1" + // } + // } + // means that FirstSupportedProtocol has three votes which support version + // 1, and one vote that supports version 2 + let mut all_count: HashMap> = HashMap::new(); + + // parse and collect all of the protos and their versions and collect them + for vote in list_of_proto_strings { + let this_vote: HashMap> = + match parse_protocols_from_string_with_no_validation(&vote) { + Ok(result) => result, + Err(_) => continue, + }; + + for (protocol, versions) in this_vote { + let supported_vers: &mut HashMap = + all_count.entry(protocol).or_insert(HashMap::new()); + + for version in versions { + let counter: &mut usize = + supported_vers.entry(version).or_insert(0); + *counter += 1; + } + } + } + + let mut final_output: HashMap = + HashMap::with_capacity(SUPPORTED_PROTOCOLS.len()); + + // Go through and remove verstions that are less than the threshold + for (protocol, versions) in all_count { + let mut meets_threshold = HashSet::new(); + for (version, count) in versions { + if count >= threshold as usize { + meets_threshold.insert(version); + } + } + + // For each protocol, compress its version list into the expected + // protocol version string format + let contracted = contract_protocol_list(&meets_threshold); + if !contracted.is_empty() { + final_output.insert(protocol, contracted); + } + } + + write_vote_to_string(&final_output) +} + +/// Return a String comprised of protocol entries in alphabetical order +/// +/// # Inputs +/// +/// * `vote`, a `HashMap` 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. +/// +/// # Returns +/// +/// A `String` whose value is series of pairs, comprising of the protocol name +/// and versions that it supports. The string takes the following format: +/// +/// "first_protocol_name=1,2-5, second_protocol_name=4,5" +/// +/// Sorts the keys in alphabetical order and creates the expected subprotocol +/// entry format. +/// +fn write_vote_to_string(vote: &HashMap) -> String { + let mut keys: Vec<&String> = vote.keys().collect(); + keys.sort(); + + let mut output = Vec::new(); + for key in keys { + // TODO error in indexing here? + output.push(format!("{}={}", key, vote[key])); + } + output.join(" ") +} + +/// Returns a boolean indicating whether the given protocol and version is +/// supported in any of the existing Tor protocols +/// +/// # Examples +/// ``` +/// use protover::*; +/// +/// let is_supported = is_supported_here(Proto::Link, 5); +/// assert_eq!(false, is_supported); +/// +/// let is_supported = is_supported_here(Proto::Link, 1); +/// assert_eq!(true, is_supported); +/// ``` +pub fn is_supported_here(proto: Proto, vers: u32) -> bool { + let currently_supported: HashMap>; + + match tor_supported() { + Ok(result) => currently_supported = result, + Err(_) => return false, + } + + let supported_versions = match currently_supported.get(&proto) { + Some(n) => n, + None => return false, + }; + + supported_versions.contains(&vers) +} + +/// Older versions of Tor cannot infer their own subprotocols +/// Used to determine which subprotocols are supported by older Tor versions. +/// +/// # Inputs +/// +/// * `version`, a string comprised of "[0-9,-]" +/// +/// # Returns +/// +/// A `String` whose value is series of pairs, comprising of the protocol name +/// and versions that it supports. 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. +/// +pub fn compute_for_old_tor(version: &str) -> String { + if c_tor_version_as_new_as( + version, + FIRST_TOR_VERSION_TO_ADVERTISE_PROTOCOLS, + ) + { + return String::new(); + } + + if c_tor_version_as_new_as(version, "0.2.9.1-alpha") { + let ret = "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"; + return String::from(ret); + } + + if c_tor_version_as_new_as(version, "0.2.7.5") { + let ret = "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"; + return String::from(ret); + } + + if c_tor_version_as_new_as(version, "0.2.4.19") { + let ret = "Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 \ + Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2"; + return String::from(ret); + } + String::new() +} + +#[cfg(test)] +mod test { + #[test] + fn test_get_versions() { + use std::collections::HashSet; + + use super::get_versions; + + assert_eq!(Err("version string is empty"), get_versions("")); + assert_eq!(Err("invalid protocol entry"), get_versions("a,b")); + assert_eq!(Err("invalid protocol entry"), get_versions("1,!")); + + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + assert_eq!(Ok(versions), get_versions("1")); + } + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + versions.insert(2); + assert_eq!(Ok(versions), get_versions("1,2")); + } + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + versions.insert(2); + versions.insert(3); + assert_eq!(Ok(versions), get_versions("1-3")); + } + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + versions.insert(2); + versions.insert(5); + assert_eq!(Ok(versions), get_versions("1-2,5")); + } + { + let mut versions: HashSet = HashSet::new(); + versions.insert(1); + versions.insert(3); + versions.insert(4); + versions.insert(5); + assert_eq!(Ok(versions), get_versions("1,3-5")); + } + } + + #[test] + fn test_contains_only_supported_protocols() { + use super::contains_only_supported_protocols; + + assert_eq!(false, contains_only_supported_protocols("")); + assert_eq!(false, contains_only_supported_protocols("Cons=")); + assert_eq!(true, contains_only_supported_protocols("Cons=1")); + assert_eq!(false, contains_only_supported_protocols("Cons=0")); + assert_eq!(false, contains_only_supported_protocols("Cons=0-1")); + assert_eq!(false, contains_only_supported_protocols("Cons=5")); + assert_eq!(false, contains_only_supported_protocols("Cons=1-5")); + assert_eq!(false, contains_only_supported_protocols("Cons=1,5")); + assert_eq!(false, contains_only_supported_protocols("Cons=5,6")); + assert_eq!(false, contains_only_supported_protocols("Cons=1,5,6")); + assert_eq!(true, contains_only_supported_protocols("Cons=1,2")); + assert_eq!(true, contains_only_supported_protocols("Cons=1-2")); + } + + #[test] + fn test_find_range() { + use super::find_range; + + assert_eq!((false, 0), find_range(&vec![])); + assert_eq!((false, 1), find_range(&vec![1])); + assert_eq!((true, 2), find_range(&vec![1, 2])); + assert_eq!((true, 3), find_range(&vec![1, 2, 3])); + assert_eq!((true, 3), find_range(&vec![1, 2, 3, 5])); + } + + #[test] + fn test_expand_version_range() { + use super::expand_version_range; + + assert_eq!(Err("version string empty"), expand_version_range("")); + assert_eq!(Ok(vec![1, 2]), expand_version_range("1-2")); + assert_eq!(Ok(vec![1, 2, 3, 4]), expand_version_range("1-4")); + assert_eq!( + Err("cannot parse protocol range lower bound"), + expand_version_range("a") + ); + assert_eq!( + Err("cannot parse protocol range upper bound"), + expand_version_range("1-a") + ); + } + + #[test] + fn test_contract_protocol_list() { + use std::collections::HashSet; + use super::contract_protocol_list; + + { + let mut versions = HashSet::::new(); + assert_eq!(String::from(""), contract_protocol_list(&versions)); + + versions.insert(1); + assert_eq!(String::from("1"), contract_protocol_list(&versions)); + + versions.insert(2); + assert_eq!(String::from("1-2"), contract_protocol_list(&versions)); + } + + { + let mut versions = HashSet::::new(); + versions.insert(1); + versions.insert(3); + assert_eq!(String::from("1,3"), contract_protocol_list(&versions)); + } + + { + let mut versions = HashSet::::new(); + versions.insert(1); + versions.insert(2); + versions.insert(3); + versions.insert(4); + assert_eq!(String::from("1-4"), contract_protocol_list(&versions)); + } + + { + let mut versions = HashSet::::new(); + versions.insert(1); + versions.insert(3); + versions.insert(5); + versions.insert(6); + versions.insert(7); + assert_eq!( + String::from("1,3,5-7"), + contract_protocol_list(&versions) + ); + } + + { + let mut versions = HashSet::::new(); + versions.insert(1); + versions.insert(2); + versions.insert(3); + versions.insert(500); + assert_eq!( + String::from("1-3,500"), + contract_protocol_list(&versions) + ); + } + } +} diff --git a/src/rust/protover/tests/protover.rs b/src/rust/protover/tests/protover.rs new file mode 100644 index 0000000000..7d8484ecc2 --- /dev/null +++ b/src/rust/protover/tests/protover.rs @@ -0,0 +1,288 @@ +extern crate protover; + +#[test] +fn parse_protocol_list_with_single_protocol_and_single_version_returns_set_of_one(){ + let protocol = "Cons=1"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_single_protocol_and_multiple_versions_returns_set_of_one(){ + let protocol = "Cons=1-2"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_different_single_protocol_and_single_version_returns_set_of_one(){ + let protocol = "HSDir=1"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_single_protocol_and_supported_version_returns_set_of_one(){ + let protocol = "Desc=2"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_two_protocols_and_single_version_returns_set_of_one(){ + let protocols = "Cons=1 HSDir=1"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + + +#[test] +fn parse_protocol_list_with_single_protocol_and_two_nonsequential_versions_returns_set_of_two(){ + let protocol = "Desc=1,2"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + + +#[test] +fn parse_protocol_list_with_single_protocol_and_two_sequential_versions_returns_set_of_two(){ + let protocol = "Desc=1-2"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_single_protocol_and_protocol_range_returns_set() { + let protocol = "Link=1-4"; + let (is_supported, unsupported) = protover::all_supported(protocol); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn parse_protocol_list_with_single_protocol_and_protocol_set() { + let protocols = "Link=3-4 Desc=2"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn protover_all_supported_with_two_values() { + let protocols = "Microdesc=1-2 Relay=2"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!("", &unsupported); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_all_supported_with_one_value() { + let protocols = "Microdesc=1-2"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!("", &unsupported); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_all_supported_with_empty() { + let protocols = ""; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(true, is_supported); + assert_eq!("", &unsupported); +} + +#[test] +fn protover_all_supported_with_three_values() { + let protocols = "LinkAuth=1 Microdesc=1-2 Relay=2"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!("", &unsupported); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_all_supported_with_unsupported_protocol() { + let protocols = "Wombat=9"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Wombat=9", &unsupported); +} + +#[test] +fn protover_all_supported_with_unsupported_versions() { + let protocols = "Link=3-999"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Link=3-999", &unsupported); +} + +#[test] +fn protover_all_supported_with_unsupported_low_version() { + let protocols = "Cons=0-1"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Cons=0-1", &unsupported); +} + +#[test] +fn protover_all_supported_with_unsupported_high_version() { + let protocols = "Cons=1-3"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Cons=1-3", &unsupported); +} + +#[test] +fn protover_all_supported_with_mix_of_supported_and_unsupproted() { + let protocols = "Link=3-4 Wombat=9"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Wombat=9", &unsupported); +} + +#[test] +fn protover_string_supports_protocol_returns_true_for_single_supported() { + let protocols = "Link=3-4 Cons=1"; + let is_supported = protover::protover_string_supports_protocol( + protocols, + protover::Proto::Cons, + 1, + ); + assert_eq!(true, is_supported); +} + +#[test] +fn protover_string_supports_protocol_returns_false_for_single_unsupported() { + let protocols = "Link=3-4 Cons=1"; + let is_supported = protover::protover_string_supports_protocol( + protocols, + protover::Proto::Cons, + 2, + ); + assert_eq!(false, is_supported); +} + +#[test] +fn protover_string_supports_protocol_returns_false_when_protocol_name_is_not_in_map(){ + let protocols = "Link=3-4"; + let is_supported = protover::protover_string_supports_protocol( + protocols, + protover::Proto::Cons, + 2, + ); + assert_eq!(false, is_supported); +} + +#[test] +fn protover_all_supported_with_unexpected_characters() { + let protocols = "Cons=*-%"; + let (is_supported, unsupported) = protover::all_supported(protocols); + assert_eq!(false, is_supported); + assert_eq!("Cons=*-%", &unsupported); +} + +#[test] +fn protover_compute_vote_returns_empty_for_empty_string() { + let protocols = vec![String::from("")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("", listed); +} + +#[test] +fn protover_compute_vote_returns_single_protocol_for_matching() { + let protocols = vec![String::from("Cons=1")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Cons=1", listed); +} + +#[test] +fn protover_compute_vote_returns_two_protocols_for_two_matching() { + let protocols = vec![String::from("Link=1 Cons=1")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Cons=1 Link=1", listed); +} + +#[test] +fn protover_compute_vote_returns_one_protocol_when_one_out_of_two_matches() { + let protocols = vec![String::from("Cons=1 Link=2"), String::from("Cons=1")]; + let listed = protover::compute_vote(protocols, 2); + assert_eq!("Cons=1", listed); +} + +#[test] +fn protover_compute_vote_returns_protocols_that_it_doesnt_currently_support() { + let protocols = vec![String::from("Foo=1 Cons=2"), String::from("Bar=1")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Bar=1 Cons=2 Foo=1", listed); +} + +#[test] +fn protover_compute_vote_returns_matching_for_mix() { + let protocols = vec![String::from("Link=1-10,500 Cons=1,3-7,8")]; + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Cons=1,3-8 Link=1-10,500", listed); +} + +#[test] +fn protover_compute_vote_returns_matching_for_longer_mix() { + let protocols = vec![ + String::from("Desc=1-10,500 Cons=1,3-7,8"), + String::from("Link=123-456,78 Cons=2-6,8 Desc=9"), + ]; + + let listed = protover::compute_vote(protocols, 1); + assert_eq!("Cons=1-8 Desc=1-10,500 Link=78,123-456", listed); +} + +#[test] +fn protover_compute_vote_returns_matching_for_longer_mix_with_threshold_two() { + let protocols = vec![ + String::from("Desc=1-10,500 Cons=1,3-7,8"), + String::from("Link=123-456,78 Cons=2-6,8 Desc=9"), + ]; + + let listed = protover::compute_vote(protocols, 2); + assert_eq!("Cons=3-6,8 Desc=9", listed); +} + +#[test] +fn protover_compute_vote_handles_duplicated_versions() { + let protocols = vec![String::from("Cons=1"), String::from("Cons=1")]; + assert_eq!("Cons=1", protover::compute_vote(protocols, 2)); + + let protocols = vec![String::from("Cons=1-2"), String::from("Cons=1-2")]; + assert_eq!("Cons=1-2", protover::compute_vote(protocols, 2)); +} + +#[test] +fn protover_compute_vote_handles_invalid_proto_entries() { + let protocols = vec![ + String::from("Cons=1"), + String::from("Cons=1"), + String::from("Link=a"), + ]; + assert_eq!("Cons=1", protover::compute_vote(protocols, 2)); + + let protocols = vec![ + String::from("Cons=1"), + String::from("Cons=1"), + String::from("Link=1-%"), + ]; + assert_eq!("Cons=1", protover::compute_vote(protocols, 2)); +} + +#[test] +fn protover_is_supported_here_returns_true_for_supported_protocol() { + assert_eq!(true, protover::is_supported_here(protover::Proto::Cons, 1)); +} + +#[test] +fn protover_is_supported_here_returns_false_for_unsupported_protocol() { + assert_eq!(false, protover::is_supported_here(protover::Proto::Cons, 5)); +} diff --git a/src/rust/smartlist/Cargo.toml b/src/rust/smartlist/Cargo.toml new file mode 100644 index 0000000000..51f486c4d7 --- /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.22" + +[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..71d89a3b87 --- /dev/null +++ b/src/rust/smartlist/lib.rs @@ -0,0 +1,5 @@ +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..5eff3fe43e --- /dev/null +++ b/src/rust/smartlist/smartlist.rs @@ -0,0 +1,100 @@ +use std::slice; +use libc::c_char; +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 +pub trait Smartlist { + fn get_list(&self) -> Vec; +} +#[repr(C)] +pub struct Stringlist { + pub list: *const *const c_char, + pub num_used: u8, + pub capacity: u8, +} + +impl Smartlist for Stringlist { + fn get_list(&self) -> Vec { + let empty: Vec = Vec::new(); + let mut v: Vec = Vec::new(); + + if self.list.is_null() { + 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. + unsafe { + let elems = + slice::from_raw_parts(self.list, self.num_used as usize); + + for i in elems.iter() { + let c_str = CStr::from_ptr(*i); + let r_str = match c_str.to_str() { + Ok(n) => n, + Err(_) => return empty, + }; + v.push(String::from(r_str)); + } + } + + v + } +} + +#[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(); + + // then, collect a pointer for the list itself + let p: *const *const c_char = p_args.as_ptr(); + + 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_util/Cargo.toml b/src/rust/tor_util/Cargo.toml index f175fbdfb0..906833ce24 100644 --- a/src/rust/tor_util/Cargo.toml +++ b/src/rust/tor_util/Cargo.toml @@ -9,5 +9,5 @@ path = "lib.rs" crate_type = ["rlib", "staticlib"] [dependencies] -libc = "*" +libc = "0.2.22" diff --git a/src/test/test_protover.c b/src/test/test_protover.c index 6ce54890d6..9ae907183b 100644 --- a/src/test/test_protover.c +++ b/src/test/test_protover.c @@ -12,6 +12,13 @@ static void test_protover_parse(void *arg) { (void) arg; +#ifdef HAVE_RUST + /** This test is disabled on rust builds, because it only exists to test + * internal C functions. */ + tt_skip(); + done: + ; +#else char *re_encoded = NULL; const char *orig = "Foo=1,3 Bar=3 Baz= Quux=9-12,14,15-16,900"; @@ -78,12 +85,18 @@ test_protover_parse(void *arg) SMARTLIST_FOREACH(elts, proto_entry_t *, ent, proto_entry_free(ent)); smartlist_free(elts); tor_free(re_encoded); +#endif } static void test_protover_parse_fail(void *arg) { (void)arg; +#ifdef HAVE_RUST + /** This test is disabled on rust builds, because it only exists to test + * internal C functions. */ + tt_skip(); +#else smartlist_t *elts; /* random junk */ @@ -109,7 +122,7 @@ test_protover_parse_fail(void *arg) /* Broken range */ elts = parse_protocol_list("Link=1,9-8,3"); tt_ptr_op(elts, OP_EQ, NULL); - +#endif done: ; } @@ -182,6 +195,32 @@ test_protover_all_supported(void *arg) tor_free(msg); } +static void +test_protover_list_supports_protocol_returns_true(void *arg) +{ + (void)arg; + + const char *protocols = "Link=1"; + int is_supported = protocol_list_supports_protocol(protocols, PRT_LINK, 1); + tt_int_op(is_supported, OP_EQ, 1); + + done: + ; +} + +static void +test_protover_list_supports_protocol_for_unsupported_returns_false(void *arg) +{ + (void)arg; + + const char *protocols = "Link=1"; + int is_supported = protocol_list_supports_protocol(protocols, PRT_LINK, 10); + tt_int_op(is_supported, OP_EQ, 0); + + done: + ; +} + #define PV_TEST(name, flags) \ { #name, test_protover_ ##name, (flags), NULL, NULL } @@ -190,6 +229,8 @@ struct testcase_t protover_tests[] = { PV_TEST(parse_fail, 0), PV_TEST(vote, 0), PV_TEST(all_supported, 0), + PV_TEST(list_supports_protocol_for_unsupported_returns_false, 0), + PV_TEST(list_supports_protocol_returns_true, 0), END_OF_TESTCASES };