Skip to content

Instantly share code, notes, and snippets.

@RoccoDev
Last active May 8, 2023 15:36
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RoccoDev/8fa130f1946f89702f799f89b8469bc9 to your computer and use it in GitHub Desktop.
Save RoccoDev/8fa130f1946f89702f799f89b8469bc9 to your computer and use it in GitHub Desktop.
Minecraft SHA-1 complement hash calculation in Rust
// Copyright (C) 2019 RoccoDev
// Licensed under the MIT license.
// <https://opensource.org/licenses/MIT>
// Bench results:
// First hash: 152ms
// Second hash: 1ms
// Third hash: 0ms
extern crate crypto; // Tested with 0.2.36
extern crate num_bigint; // Tested with 0.2
extern crate rustc_serialize; // Tested with ^0.3
extern crate regex; // Tested with 1
use regex::Regex;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use std::iter;
use rustc_serialize::hex::ToHex;
const LEADING_ZERO_REGEX: &str = r#"^0+"#;
fn calc_hash(name: &str) -> String {
let mut hasher = Sha1::new();
hasher.input_str(name);
let mut hex: Vec<u8> = iter::repeat(0).take((hasher.output_bits() + 7)/8).collect();
hasher.result(&mut hex);
let negative = (hex[0] & 0x80) == 0x80;
let regex = Regex::new(LEADING_ZERO_REGEX).unwrap();
if negative {
two_complement(&mut hex);
format!("-{}", regex.replace(hex.as_slice().to_hex().as_str(), "").to_string())
}
else {
regex.replace(hex.as_slice().to_hex().as_str(), "").to_string()
}
}
fn two_complement(bytes: &mut Vec<u8>) {
let mut carry = true;
for i in (0..bytes.len()).rev() {
bytes[i] = !bytes[i] & 0xff;
if carry {
carry = bytes[i] == 0xff;
bytes[i] = bytes[i] + 1;
}
}
}
mod tests {
#[test]
pub fn calc_hashes() {
assert_eq!("-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1", crate::calc_hash("jeb_"));
assert_eq!("4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48", crate::calc_hash("Notch"));
assert_eq!("88e16a1019277b15d58faf0541e11910eb756f6", crate::calc_hash("simon"));
}
}
@NathanHuisman
Copy link

NathanHuisman commented May 8, 2023

Another example (no regex needed):

use sha1::{Digest, Sha1};

pub fn calc_hash(name: &str) -> String {
    let mut hash: [u8; 20] = Sha1::new().chain_update(name).finalize().into();
    let negative = (hash[0] & 0x80) == 0x80;

    // Digest is 20 bytes, so 40 hex digits plus the minus sign if necessary.
    let mut hex = String::with_capacity(40 + negative as usize);
    if negative {
        hex.push('-');

        // two's complement
        let mut carry = true;
        for b in hash.iter_mut().rev() {
            (*b, carry) = (!*b).overflowing_add(carry as u8);
        }
    }
    hex.extend(
        hash.into_iter()
            // extract hex digits
            .flat_map(|x| [x >> 4, x & 0xf])
            // skip leading zeroes
            .skip_while(|&x| x == 0)
            .map(|x| char::from_digit(x as u32, 16).expect("x is always valid base16")),
    );
    hex
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_cases() {
        let pairs = &[
            ("Notch", "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"),
            ("jeb_", "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"),
            ("simon", "88e16a1019277b15d58faf0541e11910eb756f6"),
        ];
        for (input, output) in pairs {
            assert_eq!(&calc_hash(input), output);
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment