Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New object_id!(...) declarative macro that checks the validity of a const &str as ObjectID without using compile-time panic. #20214

Open
jymchng opened this issue Nov 11, 2024 · 2 comments

Comments

@jymchng
Copy link

jymchng commented Nov 11, 2024

I wrote a new declarative macro that uses the diagnostic::on_unimplemented that checks the validity of a const &str and converts it to an ObjectID under const context.

pub struct IsValidLength<const LENGTH: usize>;

#[diagnostic::on_unimplemented(
    message = "literal passed into has the wrong length, its length should exactly be 64",
)]
pub trait ValidLength {}

impl ValidLength for IsValidLength<64> {}

pub const fn is_valid_length<T: ValidLength>() {}

#[diagnostic::on_unimplemented(
    message = "the literal passed in has some invalid characters",
)]
pub trait ValidCharacters {}

pub struct HasValidLength<const VALID: bool>;

impl ValidCharacters for HasValidLength<true> {}

pub const fn has_valid_characters<T: ValidCharacters>() {}

pub const fn parse_sui_address(address: &str, start: usize) -> ([u8; 32], bool) {
    let mut trimmed_address = [0u8; 64];
    let mut i = 0;

    // Copy the remaining characters to trimmed_address
    while i < 64 && start + i < address.len() {
        trimmed_address[i] = address.as_bytes()[start + i];
        i += 1;
    }

    // Initialize the result array
    let mut bytes = [0u8; 32];
    let mut j = 0;

    while j < 32 {
        // Convert each hex character to a byte
        let byte = match (
            hex_char_to_nibble(trimmed_address[j * 2]),
            hex_char_to_nibble(trimmed_address[j * 2 + 1]),
        ) {
            (Some(high), Some(low)) => (high << 4) | low,
            _ => return ([0; 32], false),
        };
        bytes[j] = byte;
        j += 1;
    }

    (bytes, true)
}


const fn hex_char_to_nibble(c: u8) -> Option<u8> {
    match c {
        b'0'..=b'9' => Some(c - b'0'),
        b'a'..=b'f' => Some(c - b'a' + 10),
        b'A'..=b'F' => Some(c - b'A' + 10),
        _ => None,
    }
}

pub struct ObjectID([u8; 32]);

impl ObjectID {

    pub const fn new(bytes: [u8; 32]) -> Self {
        Self(bytes)
    }
}


#[macro_export]
/// Validate and convert a hex literal string into an `ObjectID`.
macro_rules! object_id {
    ($address:expr) => {{
        const ADDRESS_AND_START: (&str, usize) = check_length!($address);
        const BYTES_AND_ERR: ([u8; 32], bool) = $crate::parse_sui_address(ADDRESS_AND_START.0, ADDRESS_AND_START.1);
        const ERR: bool = BYTES_AND_ERR.1;
        $crate::has_valid_characters::<HasValidLength<ERR>>();
        $crate::ObjectID::new(BYTES_AND_ERR.0)
    }};
}

#[macro_export]
macro_rules! check_length {
    ($address:expr) => {{
        const START: usize = 0;
        const ADDRESS_BYTES_LENGTH_AND_START: (usize, usize) = if $address.len() >= 2 && $address.as_bytes()[0] == b'0' && $address.as_bytes()[1] == b'x' {
            const START: usize = 2;
            ($address.as_bytes().len() - START, START)
        } else {
            ($address.as_bytes().len() - START, START)
        };
        const ADDRESS_BYTES_LENGTH: usize = ADDRESS_BYTES_LENGTH_AND_START.0;
        is_valid_length::<IsValidLength<ADDRESS_BYTES_LENGTH>>();
        ($address, ADDRESS_BYTES_LENGTH_AND_START.1)
    }}
}

fn main() {
    // valid:
    object_id!("0xd1ec56d5d92d3e3d74ce6e50e1b2a6b505bedd7d7305ead61d5093619bedb2a7");
    
    // invalid character 'G'
    object_id!("0xd1ec56d5d92d3e3d74ce6e50e1b2a6b505beGG7d7305ead61d5093619bedb2a7");
                                                   // ^^ here bro
                                                   
    // invalid length
    object_id!("0xd1ec56d5d92d3e3d74ce6e50e1b2a6b505beGG7d7305ead61d5093619bedb2a7abcabcabc");
                                                                                 //  ^^^^^^ 6 characters more
}

Playground.

Have a look @patrickkuo and @sblackshear. Thank you.

@jymchng
Copy link
Author

jymchng commented Nov 11, 2024

Advantage here is that it does not rely on compile time panics and if relying on Rust's version < 1.80, we can always remove the #[diagnostic::on_unimplemented] - it's less beautiful but it works.

@jymchng
Copy link
Author

jymchng commented Nov 12, 2024

@bmwill

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant