From bd78d2349585ce0ad68663afa902d6e8c83855f3 Mon Sep 17 00:00:00 2001 From: Jacob Neil Taylor Date: Sat, 1 Jun 2024 15:40:05 +1000 Subject: [PATCH 1/5] Add support for building dictionaries from string slices --- src/protocol/dictionary.rs | 290 ++++++++++++++++++++++++++++++++++--- 1 file changed, 273 insertions(+), 17 deletions(-) diff --git a/src/protocol/dictionary.rs b/src/protocol/dictionary.rs index 8691da1..8f517bd 100644 --- a/src/protocol/dictionary.rs +++ b/src/protocol/dictionary.rs @@ -127,29 +127,48 @@ pub struct Dictionary { #[allow(unused)] impl Dictionary { + fn from_lines(lines: StringIterator) -> Result { + let mut attributes: Vec = Vec::new(); + let mut values: Vec = Vec::new(); + let mut vendors: Vec = Vec::new(); + + match parse_lines(lines, &mut attributes, &mut values, &mut vendors) { + Ok(()) => Ok(Dictionary { attributes, values, vendors }), + Err(error) => Err(error), + } + } + /// Creates Dictionary from a string pub fn from_str(dictionary_str: &str) -> Result { - todo!() + let lines = read_str(dictionary_str); + Dictionary::from_lines(lines) } /// Creates Dictionary from a RADIUS dictionary file pub fn from_file(file_path: &str) -> Result { - let mut attributes: Vec = Vec::new(); - let mut values: Vec = Vec::new(); - let mut vendors: Vec = Vec::new(); - - match parse_file(file_path, &mut attributes, &mut values, &mut vendors) { - Ok(()) => Ok(Dictionary { attributes, values, vendors }), + match read_file(file_path) { + Ok(lines) => Dictionary::from_lines(lines), Err(error) => Err(error) } } + /// The add functions process attributes, values and vendors from a supplied dictionary file + /// and merge them into an existing set of attributes, values and vendors + + /// Adds a dictionary string to existing Dictionary + pub fn add_str(&mut self, dictionary_str: &str) -> Result<(), RadiusError> { + let lines = read_str(dictionary_str); + parse_lines(lines, &mut self.attributes, &mut self.values, &mut self.vendors) + } + /// Adds a dictionary file to existing Dictionary - /// - /// Processes attributes, values and vendors from supplied dictionary file - /// and adds them to existing attributes, values and vendors pub fn add_file(&mut self, file_path: &str) -> Result<(), RadiusError> { - parse_file(file_path, &mut self.attributes, &mut self.values, &mut self.vendors) + match read_file(file_path) { + Ok(lines) => parse_lines( + lines, &mut self.attributes, &mut self.values, &mut self.vendors + ), + Err(error) => Err(error) + } } /// Returns parsed DictionaryAttributes @@ -191,14 +210,29 @@ fn assign_attribute_type(code_type: &str) -> Option { } } -fn parse_file(file_path: &str, attributes: &mut Vec, values: &mut Vec, vendors: &mut Vec) -> Result<(), RadiusError> { - let mut vendor_name: String = String::new(); +type StringIterator = Box>; + +fn filter_lines + 'static>(lines: T) -> StringIterator { + Box::new( + lines + .filter(|line| !line.is_empty()) + .filter(|line| !line.contains(&COMMENT_PREFIX)) + ) +} +fn read_file(file_path: &str) -> Result { let reader = io::BufReader::new(File::open(file_path).map_err(|error| RadiusError::MalformedDictionaryError { error })?); - let lines = reader.lines() - .filter_map(Result::ok) - .filter(|line| !line.is_empty()) - .filter(|line| !line.contains(&COMMENT_PREFIX)); + Ok(filter_lines(reader.lines().filter_map(Result::ok))) +} + +fn read_str(dictionary_str: &str) -> StringIterator { + let lines: Vec = dictionary_str.to_string().lines() + .map(|line| line.to_owned()).collect(); + filter_lines(lines.into_iter()) +} + +fn parse_lines(lines: StringIterator, attributes: &mut Vec, values: &mut Vec, vendors: &mut Vec) -> Result<(), RadiusError>{ + let mut vendor_name: String = String::new(); for line in lines { let parsed_line: Vec<&str> = line.split_whitespace().filter(|&item| !item.is_empty()).collect(); @@ -249,6 +283,116 @@ fn parse_vendor(parsed_line: Vec<&str>, vendors: &mut Vec) { mod tests { use super::*; + #[test] + fn test_from_str() { + let dictionary_str = include_str!("../../dict_examples/test_dictionary_dict"); + + let dict = Dictionary::from_str(dictionary_str).unwrap(); + + let mut attributes: Vec = Vec::new(); + attributes.push(DictionaryAttribute { + name: "User-Name".to_string(), + vendor_name: "".to_string(), + code: 1, + code_type: Some(SupportedAttributeTypes::AsciiString) + }); + attributes.push(DictionaryAttribute { + name: "NAS-IP-Address".to_string(), + vendor_name: "".to_string(), + code: 4, + code_type: Some(SupportedAttributeTypes::IPv4Addr) + }); + attributes.push(DictionaryAttribute { + name: "NAS-Port-Id".to_string(), + vendor_name: "".to_string(), + code: 5, + code_type: Some(SupportedAttributeTypes::Integer) + }); + attributes.push(DictionaryAttribute { + name: "Framed-Protocol".to_string(), + vendor_name: "".to_string(), + code: 7, + code_type: Some(SupportedAttributeTypes::Integer) + }); + attributes.push(DictionaryAttribute { + name: "Chargeable-User-Identity".to_string(), + vendor_name: "".to_string(), + code: 89, + code_type: Some(SupportedAttributeTypes::ByteString) + }); + attributes.push(DictionaryAttribute { + name: "Delegated-IPv6-Prefix".to_string(), + vendor_name: "".to_string(), + code: 123, + code_type: Some(SupportedAttributeTypes::IPv6Prefix) + }); + attributes.push(DictionaryAttribute { + name: "MIP6-Feature-Vector".to_string(), + vendor_name: "".to_string(), + code: 124, + code_type: Some(SupportedAttributeTypes::Integer64) + }); + attributes.push(DictionaryAttribute { + name: "Mobile-Node-Identifier".to_string(), + vendor_name: "".to_string(), + code: 145, + code_type: Some(SupportedAttributeTypes::ByteString) + }); + attributes.push(DictionaryAttribute { + name: "PMIP6-Home-Interface-ID".to_string(), + vendor_name: "".to_string(), + code: 153, + code_type: Some(SupportedAttributeTypes::InterfaceId) + }); + attributes.push(DictionaryAttribute { + name: "PMIP6-Home-IPv4-HoA".to_string(), + vendor_name: "".to_string(), + code: 155, + code_type: Some(SupportedAttributeTypes::IPv4Prefix) + }); + attributes.push(DictionaryAttribute { + name: "Somevendor-Name".to_string(), + vendor_name: "Somevendor".to_string(), + code: 1, + code_type: Some(SupportedAttributeTypes::AsciiString) + }); + attributes.push(DictionaryAttribute { + name: "Somevendor-Number".to_string(), + vendor_name: "Somevendor".to_string(), + code: 2, + code_type: Some(SupportedAttributeTypes::Integer) + }); + attributes.push(DictionaryAttribute { + name: "Class".to_string(), + vendor_name: "".to_string(), + code: 25, + code_type: Some(SupportedAttributeTypes::ByteString) + }); + + let mut values: Vec = Vec::new(); + values.push(DictionaryValue { + attribute_name: "Framed-Protocol".to_string(), + value_name: "PPP".to_string(), + vendor_name: "".to_string(), + value: "1".to_string() + }); + values.push(DictionaryValue { + attribute_name: "Somevendor-Number".to_string(), + value_name: "Two".to_string(), + vendor_name: "Somevendor".to_string(), + value: "2".to_string() + }); + + let mut vendors: Vec = Vec::new(); + vendors.push(DictionaryVendor { + name: "Somevendor".to_string(), + id: 10, + }); + + let expected_dict = Dictionary { attributes, values, vendors }; + assert_eq!(dict, expected_dict) + } + #[test] fn test_from_file() { let dictionary_path = "./dict_examples/test_dictionary_dict"; @@ -359,6 +503,118 @@ mod tests { assert_eq!(dict, expected_dict) } + #[test] + fn test_add_str() { + let empty_dictionary_str = include_str!("../../dict_examples/empty_test_dictionary_dict"); + let dictionary_str = include_str!("../../dict_examples/test_dictionary_dict"); + + let mut dict = Dictionary::from_str(empty_dictionary_str).unwrap(); + dict.add_str(dictionary_str).unwrap(); + + let mut attributes: Vec = Vec::new(); + attributes.push(DictionaryAttribute { + name: "User-Name".to_string(), + vendor_name: "".to_string(), + code: 1, + code_type: Some(SupportedAttributeTypes::AsciiString) + }); + attributes.push(DictionaryAttribute { + name: "NAS-IP-Address".to_string(), + vendor_name: "".to_string(), + code: 4, + code_type: Some(SupportedAttributeTypes::IPv4Addr) + }); + attributes.push(DictionaryAttribute { + name: "NAS-Port-Id".to_string(), + vendor_name: "".to_string(), + code: 5, + code_type: Some(SupportedAttributeTypes::Integer) + }); + attributes.push(DictionaryAttribute { + name: "Framed-Protocol".to_string(), + vendor_name: "".to_string(), + code: 7, + code_type: Some(SupportedAttributeTypes::Integer) + }); + attributes.push(DictionaryAttribute { + name: "Chargeable-User-Identity".to_string(), + vendor_name: "".to_string(), + code: 89, + code_type: Some(SupportedAttributeTypes::ByteString) + }); + attributes.push(DictionaryAttribute { + name: "Delegated-IPv6-Prefix".to_string(), + vendor_name: "".to_string(), + code: 123, + code_type: Some(SupportedAttributeTypes::IPv6Prefix) + }); + attributes.push(DictionaryAttribute { + name: "MIP6-Feature-Vector".to_string(), + vendor_name: "".to_string(), + code: 124, + code_type: Some(SupportedAttributeTypes::Integer64) + }); + attributes.push(DictionaryAttribute { + name: "Mobile-Node-Identifier".to_string(), + vendor_name: "".to_string(), + code: 145, + code_type: Some(SupportedAttributeTypes::ByteString) + }); + attributes.push(DictionaryAttribute { + name: "PMIP6-Home-Interface-ID".to_string(), + vendor_name: "".to_string(), + code: 153, + code_type: Some(SupportedAttributeTypes::InterfaceId) + }); + attributes.push(DictionaryAttribute { + name: "PMIP6-Home-IPv4-HoA".to_string(), + vendor_name: "".to_string(), + code: 155, + code_type: Some(SupportedAttributeTypes::IPv4Prefix) + }); + attributes.push(DictionaryAttribute { + name: "Somevendor-Name".to_string(), + vendor_name: "Somevendor".to_string(), + code: 1, + code_type: Some(SupportedAttributeTypes::AsciiString) + }); + attributes.push(DictionaryAttribute { + name: "Somevendor-Number".to_string(), + vendor_name: "Somevendor".to_string(), + code: 2, + code_type: Some(SupportedAttributeTypes::Integer) + }); + attributes.push(DictionaryAttribute { + name: "Class".to_string(), + vendor_name: "".to_string(), + code: 25, + code_type: Some(SupportedAttributeTypes::ByteString) + }); + + let mut values: Vec = Vec::new(); + values.push(DictionaryValue { + attribute_name: "Framed-Protocol".to_string(), + value_name: "PPP".to_string(), + vendor_name: "".to_string(), + value: "1".to_string() + }); + values.push(DictionaryValue { + attribute_name: "Somevendor-Number".to_string(), + value_name: "Two".to_string(), + vendor_name: "Somevendor".to_string(), + value: "2".to_string() + }); + + let mut vendors: Vec = Vec::new(); + vendors.push(DictionaryVendor { + name: "Somevendor".to_string(), + id: 10, + }); + + let expected_dict = Dictionary { attributes, values, vendors }; + assert_eq!(dict, expected_dict) + } + #[test] fn test_add_file() { let empty_dictionary_path = "./dict_examples/empty_test_dictionary_dict"; From 47c1ce044affa9f9a3dc43dc74e229d9318d2689 Mon Sep 17 00:00:00 2001 From: MikhailMS <1488maiklm@gmail.com> Date: Sat, 28 Sep 2024 00:10:40 +0100 Subject: [PATCH 2/5] Closes #8 --- src/tools/mod.rs | 69 +++++++++++++++--------------------------------- 1 file changed, 22 insertions(+), 47 deletions(-) diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 6ee36f9..71f1b6c 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -253,23 +253,10 @@ pub fn decrypt_data(data: &[u8], authenticator: &[u8], secret: &[u8]) -> Vec return Vec::new() } - let mut result = Vec::with_capacity(data.len()); - let mut prev_result = authenticator; - let mut hash = [0u8; 16]; + let mut result = Vec::with_capacity(data.len()); + let prev_result = authenticator; - for data_chunk in data.chunks_exact(16) { - let mut md5 = Md5::new(); - md5.update(secret); - md5.update(prev_result); - hash.copy_from_slice(&md5.finalize()); - - for (_data, _hash) in data_chunk.iter().zip(hash.iter_mut()) { - *_hash ^= _data - } - - result.extend_from_slice(&hash); - prev_result = data_chunk; - } + decrypt_helper(data, prev_result, &mut result, secret); while result[result.len()-1] == 0 { result.pop(); @@ -328,23 +315,10 @@ pub fn salt_decrypt_data(data: &[u8], authenticator: &[u8], secret: &[u8]) -> Re salted_authenticator[..16].copy_from_slice(authenticator); salted_authenticator[16..].copy_from_slice(&data[..2]); - let mut hash = [0u8; 16]; let mut result = Vec::with_capacity(data.len()-2); - let mut prev_result = &salted_authenticator[..]; - - for data_chunk in (&data[2..]).chunks_exact(16) { - let mut md5 = Md5::new(); - md5.update(secret); - md5.update(prev_result); - hash.copy_from_slice(&md5.finalize()); - - for (_data, _hash) in data_chunk.iter().zip(hash.iter_mut()) { - *_hash ^= _data - } - result.extend_from_slice(&hash); + let prev_result = &salted_authenticator[..]; - prev_result = data_chunk; - } + decrypt_helper(&data[2..], prev_result, &mut result, secret); let target_len = usize::from(result.remove(0)); @@ -376,22 +350,23 @@ fn encrypt_helper<'a:'b, 'b>(mut out: &'a mut [u8], mut result: &'b [u8], hash: } } -// WIP -// fn decrypt_helper<'a:'b, 'b>(data: &'a mut [u8], mut prev_result: &'b [u8], result: &mut Vec, mut hash: &mut[u8], secret: &[u8]) { -// for data_chunk in data.chunks_exact(16) { -// let mut md5 = Md5::new(); -// md5.input(secret); -// md5.input(prev_result); -// md5.result(&mut hash); - -// for (_data, _hash) in data_chunk.iter().zip(hash.iter_mut()) { -// *_hash ^= _data -// } - -// result.extend_from_slice(&hash); -// prev_result = data_chunk; -// } -// } +fn decrypt_helper<'a:'b, 'b>(data: &'a [u8], mut prev_result: &'b [u8], result: &mut Vec, secret: &[u8]) { + let mut hash = [0u8; 16]; + + for data_chunk in data.chunks_exact(16) { + let mut md5 = Md5::new(); + md5.update(secret); + md5.update(prev_result); + hash.copy_from_slice(&md5.finalize()); + + for (_data, _hash) in data_chunk.iter().zip(hash.iter_mut()) { + *_hash ^= _data; + } + + result.extend_from_slice(&hash); + prev_result = data_chunk; + } +} fn u16_to_be_bytes(u16_data: u16) -> [u8;2] { u16_data.to_be_bytes() From 4b28bd57495b7e5c429d8f82b032cb7b1fe999c8 Mon Sep 17 00:00:00 2001 From: MikhailMS <1488maiklm@gmail.com> Date: Sat, 28 Sep 2024 00:20:30 +0100 Subject: [PATCH 3/5] Update Rust version list in Github Actions --- .github/workflows/rust-radius.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust-radius.yml b/.github/workflows/rust-radius.yml index 38e71ac..1b9506f 100644 --- a/.github/workflows/rust-radius.yml +++ b/.github/workflows/rust-radius.yml @@ -27,7 +27,10 @@ jobs: strategy: fail-fast: false matrix: - rust: [1.65.0, 1.66.1, 1.67.1, 1.68.2, 1.69.0, 1.70.0, 1.71.0, 1.71.1, 1.72.0, 1.72.1, 1.73.0, 1.74.0, 1.74.1, 1.75.0, 1.76.0, 1.77.0] + rust: [1.66.1, 1.67.1, 1.68.2, 1.69.0, + 1.70.0, 1.71.0, 1.71.1, 1.72.0, 1.72.1, 1.73.0, 1.74.0, 1.74.1, 1.75.0, 1.76.0, 1.77.0, 1.77.1, 1.77.2, 1.78.0, 1.79.0, + 1.80.0, 1.80.1, 1.81.0, 1.82.0, 1.83.0 + ] os: [ubuntu-20.04] steps: From 89c5c0adac801cd8c8a40a30edf852dea30193a2 Mon Sep 17 00:00:00 2001 From: MikhailMS <1488maiklm@gmail.com> Date: Sat, 28 Sep 2024 01:29:13 +0100 Subject: [PATCH 4/5] Update Rust version list in Github Actions --- .github/workflows/rust-radius.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust-radius.yml b/.github/workflows/rust-radius.yml index 1b9506f..3a6070e 100644 --- a/.github/workflows/rust-radius.yml +++ b/.github/workflows/rust-radius.yml @@ -29,7 +29,7 @@ jobs: matrix: rust: [1.66.1, 1.67.1, 1.68.2, 1.69.0, 1.70.0, 1.71.0, 1.71.1, 1.72.0, 1.72.1, 1.73.0, 1.74.0, 1.74.1, 1.75.0, 1.76.0, 1.77.0, 1.77.1, 1.77.2, 1.78.0, 1.79.0, - 1.80.0, 1.80.1, 1.81.0, 1.82.0, 1.83.0 + 1.80.0, 1.80.1, 1.81.0 ] os: [ubuntu-20.04] steps: From 581e51494294328291dd2f42619b28dd7047794d Mon Sep 17 00:00:00 2001 From: MikhailMS <1488maiklm@gmail.com> Date: Sun, 29 Sep 2024 20:04:25 +0100 Subject: [PATCH 5/5] Prepare next release 0.4.4 --- CHANGELOG.md | 25 +++++++++++++++++++++++-- Cargo.toml | 4 ++-- README.md | 4 ++-- src/protocol/dictionary.rs | 33 ++++++++++++++++----------------- 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68d1849..b2d02bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,29 @@ +============= +# v0.4.4 (29 Sep 2024) + +This release: +* fixes issue reported in [#8](/../../issues/8) +* includes a [PR](/../../pull/31) (thanks to @jacobneiltaylor for proposing the PR) + +## What's new +* [PR](/../../pull/31) added new functions to work with `Dictionary`: + * `from_str` - creates an intsance of `Dictionary` from dictionary string + * `add_str` - reads dictionary string and adds it to an existing instance of `Dictionary` +* Added `1.77.1, 1.77.2, 1.78.0, 1.79.0, 1.80.0, 1.80.1, 1.81.0` Rust versions to Action pipeline + +## What's removed or deprecated +* Removed `1.65.0` Rust version from Github Actions (this version is still supported by library) + +## What's changed +* [PR](/../../pull/31) also improves line parsing logic of `Dictionary` to be more generic +* [Reworked](/../../issues/8) `decrypt_data` & `salt_decrypt_data` functions - extracted shared code into `decrypt_helper` + + ============= # v0.4.3 (24 Mar 2024) This release fixes issues reported in: -* [#28](/../../issues/28) (thanks to CoderChristopher for reporting and suggesting the solution) +* [#28](/../../issues/28) (thanks to @CoderChristopher for reporting and suggesting the solution) * [#27](/../../issues/27) ## What's new @@ -110,7 +131,7 @@ users to decide on the crates they want to use to get UdpSockets, async and runt ## What's changed * Breaking change - **client** module now only has Generic RADIUS Client implementation * Breaking change - **server** module now only has Generic RADIUS Server implementation -* Breaking change - **RadiusMsgType** code as been moved from **servers** module into **radius_packet** module +* Breaking change - **RadiusMsgType** code has been moved from **servers** module into **radius_packet** module * Breaking change - **get** prefix was removed for all functions where it was used before ([C-GETTER Rust convention](https://rust-lang.github.io/api-guidelines/naming.html#c-getter)) * Breaking change - **client** & **server** implementations now require related traits to be implemented. For more information have a look into `examples/` * All RADIUS defined errors now have *Error* suffix, ie **MalformedPacketError** diff --git a/Cargo.toml b/Cargo.toml index 32e2efa..bf499d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["MikhailMS <1488maiklm@gmail.com>"] categories = ["network-programming"] description = "Pure Rust implementation of RADIUS Server/Client" -documentation = "https://docs.rs/radius-rust/0.4.3" +documentation = "https://docs.rs/radius-rust/0.4.4" edition = "2018" include = [ "Cargo.toml", @@ -20,7 +20,7 @@ license = "MIT" name = "radius-rust" readme = "README.md" repository = "https://github.com/MikhailMS/rust-radius" -version = "0.4.3" +version = "0.4.4" [features] # Default doesn\t include anythin - keep it simple diff --git a/README.md b/README.md index d02ffdd..9819a65 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,12 @@ Rationale behind this project: ## Installation ``` [dependencies] -radius-rust = "0.4.3" +radius-rust = "0.4.4" OR if you are planning to build Async RADIUS Client/Server [dependencies] -radius-rust = { version = "0.4.3", features = ["async-radius"] } +radius-rust = { version = "0.4.4", features = ["async-radius"] } ``` diff --git a/src/protocol/dictionary.rs b/src/protocol/dictionary.rs index 8f517bd..cae7a68 100644 --- a/src/protocol/dictionary.rs +++ b/src/protocol/dictionary.rs @@ -127,18 +127,7 @@ pub struct Dictionary { #[allow(unused)] impl Dictionary { - fn from_lines(lines: StringIterator) -> Result { - let mut attributes: Vec = Vec::new(); - let mut values: Vec = Vec::new(); - let mut vendors: Vec = Vec::new(); - - match parse_lines(lines, &mut attributes, &mut values, &mut vendors) { - Ok(()) => Ok(Dictionary { attributes, values, vendors }), - Err(error) => Err(error), - } - } - - /// Creates Dictionary from a string + /// Creates Dictionary from a RADIUS dictionary string pub fn from_str(dictionary_str: &str) -> Result { let lines = read_str(dictionary_str); Dictionary::from_lines(lines) @@ -152,16 +141,15 @@ impl Dictionary { } } - /// The add functions process attributes, values and vendors from a supplied dictionary file - /// and merge them into an existing set of attributes, values and vendors - - /// Adds a dictionary string to existing Dictionary + /// Processes attributes, values and vendors from a supplied dictionary string and + /// adds those to attributes, values and vendors of an existing Dictionary pub fn add_str(&mut self, dictionary_str: &str) -> Result<(), RadiusError> { let lines = read_str(dictionary_str); parse_lines(lines, &mut self.attributes, &mut self.values, &mut self.vendors) } - /// Adds a dictionary file to existing Dictionary + /// Processes attributes, values and vendors from a supplied dictionary file and + /// adds those to attributes, values and vendors of an existing Dictionary pub fn add_file(&mut self, file_path: &str) -> Result<(), RadiusError> { match read_file(file_path) { Ok(lines) => parse_lines( @@ -185,6 +173,17 @@ impl Dictionary { pub fn vendors(&self) -> &[DictionaryVendor] { &self.vendors } + + fn from_lines(lines: StringIterator) -> Result { + let mut attributes: Vec = Vec::new(); + let mut values: Vec = Vec::new(); + let mut vendors: Vec = Vec::new(); + + match parse_lines(lines, &mut attributes, &mut values, &mut vendors) { + Ok(()) => Ok(Dictionary { attributes, values, vendors }), + Err(error) => Err(error), + } + } } fn assign_attribute_type(code_type: &str) -> Option {