Where is the equivalent in Rust of the msgpack Timestamp class in Python

112 Views Asked by At

In Python, when decoding a messagepack message that contains a timestamp, the code in the msgpack library (Timestamp class) is pretty straightforward as it just takes the bytes it finds for a field and converts them as copied from the lib below:

def from_bytes(b):
    """Unpack bytes into a `Timestamp` object.

    Used for pure-Python msgpack unpacking.

    :param b: Payload from msgpack ext message with code -1
    :type b: bytes

    :returns: Timestamp object unpacked from msgpack ext payload
    :rtype: Timestamp
    """
    if len(b) == 4:
        seconds = struct.unpack("!L", b)[0]
        nanoseconds = 0
    elif len(b) == 8:
        data64 = struct.unpack("!Q", b)[0]
        seconds = data64 & 0x00000003FFFFFFFF
        nanoseconds = data64 >> 34
    elif len(b) == 12:
        nanoseconds, seconds = struct.unpack("!Iq", b)
    else:
        raise ValueError(
            "Timestamp type can only be created from 32, 64, or 96-bit byte objects"
        )
    return Timestamp(seconds, nanoseconds)

I am massively struggling to achieve the same behavior in Rust. For simplicity's sake, this a simple struct:

#[derive(Debug, PartialEq, Deserialize)]
struct ReceivedMessage {
    a_string: String,
    a_value: f64,
    a_timestamp: ?????      // <--- what to put here?
}

When calling rmp_serde::from_slice(&'a [u8]) with the binary messagepack message I get

called Result::unwrap() on an Err value: Syntax("invalid type: newtype struct, expected byte array")

and it just doesn't matter what type I put for a_timestamp. I tried SystemTime, i64, f64 and serde_as with TimestampSeconds etc. I then tried using Vec<u8> and serde_bytes as well as writing a custom converter with serde::conv! All to no avail and always the same error message. There doesn't seem to be any comprehensive tutorial online on this. (The other fields in the struct deserialize just fine as I found out when debugging).
If it matters, the binary message comes as a map, i.e. with the field names and not as an array.

It seems to me that this should be easy as Python has all this functionality built-in into the msgpack library. So how can I decode a timestamp in Rust?

I am using the newest crate versions

rmp-serde = "1.1.2"
serde = "1"
serde_derive = "1"
serde_with = "3.4"
serde_bytes = "0.11"
1

There are 1 best solutions below

4
On BEST ANSWER

rmp-serde doesn't have this feature implemented, see #298.

There is some code from the issue that appears to follow the Msgpack standard:

#[derive(Debug, Serialize, Deserialize)]
struct SomeStruct {
    #[serde(
        serialize_with = "serialize_mp_date_secs",
        deserialize_with = "deserialize_mp_date_secs"
    )]
    date: u64,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename = "_ExtStruct")]
pub struct ExtStruct((i8, ByteBuf));

fn u32_from_bytebuf(buf: &ByteBuf) -> Result<u32, TryFromSliceError> {
    let bytes = buf.as_slice().try_into()?;
    Ok(u32::from_be_bytes(bytes))
}

fn u64_from_bytebuf(buf: &ByteBuf) -> Result<u64, TryFromSliceError> {
    let bytes = buf.as_slice().try_into()?;
    Ok(u64::from_be_bytes(bytes))
}

pub fn serialize_mp_date_secs<S>(secs: &u64, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let data64 = *secs;
    let ext = if (secs >> 34) == 0 {
        if (data64 & 0xffffffff00000000) == 0 {
            // timestamp 32
            let data32 = data64 as u32;
            ExtStruct((-1, ByteBuf::from(data32.to_be_bytes())))
        } else {
            // timestamp 64
            ExtStruct((-1, ByteBuf::from(data64.to_be_bytes())))
        }
    } else {
        // timestamp 96
        let mut bytes = 0u32.to_be_bytes().to_vec();
        let mut secs = (data64 as i64).to_be_bytes().to_vec();
        bytes.append(&mut secs);
        ExtStruct((-1, ByteBuf::from(bytes)))
    };
    ext.serialize(serializer)
}

pub fn deserialize_mp_date_secs<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
    D: Deserializer<'de>,
{
    let ExtStruct((ext_type, buf)) = Deserialize::deserialize(deserializer)?;
    if ext_type != -1 {
        return Err(de::Error::custom("Invalid extension type (!=-1)"));
    }

    let sec = match buf.len() {
        4 => {
            let sec = u32_from_bytebuf(&buf).map_err(|e| {
                de::Error::custom(format!("Failed to get u32 from bytebuf ({})", e))
            })?;
            sec as u64
        }
        8 => {
            let data64 = u64_from_bytebuf(&buf).map_err(|e| {
                de::Error::custom(format!("Failed to get u64 from bytebuf ({})", e))
            })?;
            data64 & 0x00000003ffffffff
        }
        12 => {
            u64_from_bytebuf(&buf)
                .map_err(|e| de::Error::custom(format!("Failed to get u64 from bytebuf ({})", e)))?
                + 4
        }
        n => {
            return Err(de::Error::custom(format!(
                "Invalid data len {n} (valid sizes are 4, 8 and 12)"
            )))
        }
    };

    Ok(sec)
}

However, this seems to only care for precision up to the second.