Skip to content

Commit

Permalink
feat: implement FromStr for TLSH objects
Browse files Browse the repository at this point in the history
This allows building a TLSH struct from an existing hash.
  • Loading branch information
vthib committed Jul 30, 2023
1 parent f242ca9 commit 0c6a9ff
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 11 deletions.
57 changes: 57 additions & 0 deletions src/tlsh.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use core::str::FromStr;

use crate::pearson::{b_mapping, fast_b_mapping};
use crate::quartile::get_quartiles;
use crate::util::{l_capturing, swap_byte};
Expand Down Expand Up @@ -343,6 +345,61 @@ impl<const TLSH_CHECKSUM_LEN: usize, const TLSH_STRING_LEN_REQ: usize, const COD

diff
}

fn from_hash(s: &[u8]) -> Option<Self> {
if s.len() != TLSH_STRING_LEN_REQ || s[0] != b'T' || s[1] != b'1' {
return None;
}

let mut i = 2;

let mut checksum = [0; TLSH_CHECKSUM_LEN];
for k in &mut checksum {
*k = swap_byte(from_hex(s, &mut i)?);
}

let lvalue = swap_byte(from_hex(s, &mut i)?);
let qb = from_hex(s, &mut i)?;
let q1_ratio = qb >> 4;
let q2_ratio = qb & 0x0F;

let mut code = [0; CODE_SIZE];
for c in code.iter_mut().rev() {
*c = from_hex(s, &mut i)?;
}

Some(Self {
lvalue,
q1_ratio,
q2_ratio,
checksum,
code,
})
}
}

/// Error returned when failing to convert a hash string to a `Tlsh` object.
#[derive(Debug, PartialEq, Eq)]
pub struct ParseError;

/// Parse a hash string and build the corresponding `Tlsh` object.
impl<const TLSH_CHECKSUM_LEN: usize, const TLSH_STRING_LEN_REQ: usize, const CODE_SIZE: usize>
FromStr for Tlsh<TLSH_CHECKSUM_LEN, TLSH_STRING_LEN_REQ, CODE_SIZE>
{
type Err = ParseError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_hash(s.as_bytes()).ok_or(ParseError)
}
}

fn from_hex(s: &[u8], i: &mut usize) -> Option<u8> {
let a = char::from(s[*i]).to_digit(16)?;
*i += 1;
let b = char::from(s[*i]).to_digit(16)?;
*i += 1;

Some(((a as u8) << 4) | (b as u8))
}

fn to_hex(s: &mut [u8], s_idx: &mut usize, b: u8) {
Expand Down
52 changes: 41 additions & 11 deletions tests/it/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,55 @@ where
}

macro_rules! do_hash_test {
($testname:ident, $name:expr, $type:ty) => {
($testname:ident, $name:expr, $builder:ty, $tlsh:ty) => {
#[test]
fn $testname() {
test_hash(
&format!("tests/assets/tlsh/exp/example_data.{}.len.out_EXP", $name),
|contents| {
let mut tlsh = <$type>::new();
tlsh.update(contents);
tlsh.build()
.map(|v| String::from_utf8(v.hash().to_vec()).unwrap())
.unwrap_or_default()
let mut builder = <$builder>::new();
builder.update(contents);
let tlsh = builder.build().unwrap();
let hash = String::from_utf8(tlsh.hash().to_vec()).unwrap();

// Test the FromStr implementation
let tlsh2 = hash.parse::<$tlsh>().unwrap();
assert_eq!(tlsh.hash(), tlsh2.hash());

hash
},
)
}
};
}

do_hash_test!(test_hash_48_1, "48.1", tlsh2::TlshBuilder48_1);
do_hash_test!(test_hash_128_1, "128.1", tlsh2::TlshBuilder128_1);
do_hash_test!(test_hash_128_3, "128.3", tlsh2::TlshBuilder128_3);
do_hash_test!(test_hash_256_1, "256.1", tlsh2::TlshBuilder256_1);
do_hash_test!(test_hash_256_3, "256.3", tlsh2::TlshBuilder256_3);
do_hash_test!(
test_hash_48_1,
"48.1",
tlsh2::TlshBuilder48_1,
tlsh2::Tlsh48_1
);
do_hash_test!(
test_hash_128_1,
"128.1",
tlsh2::TlshBuilder128_1,
tlsh2::Tlsh128_1
);
do_hash_test!(
test_hash_128_3,
"128.3",
tlsh2::TlshBuilder128_3,
tlsh2::Tlsh128_3
);
do_hash_test!(
test_hash_256_1,
"256.1",
tlsh2::TlshBuilder256_1,
tlsh2::Tlsh256_1
);
do_hash_test!(
test_hash_256_3,
"256.3",
tlsh2::TlshBuilder256_3,
tlsh2::Tlsh256_3
);

0 comments on commit 0c6a9ff

Please sign in to comment.