bit-twiddler 0.2.0

Cross-platform developer toolbox: bit manipulation, hashing, YAML/JSON/SQL, QR, Markdown, cron, and 40+ more tools — Tauri v2, no Node.js
use serde::Serialize;

#[derive(Serialize)]
#[serde(tag = "kind")]
pub enum ProtoValue {
    #[serde(rename = "varint")]
    Varint { value: u64 },
    #[serde(rename = "fixed64")]
    Fixed64 { value: u64 },
    #[serde(rename = "fixed32")]
    Fixed32 { value: u32 },
    #[serde(rename = "bytes")]
    Bytes {
        hex: String,
        text: Option<String>,
        nested: Option<Vec<ProtoField>>,
    },
}

#[derive(Serialize)]
pub struct ProtoField {
    pub field_number: u64,
    pub wire_type: u8,
    pub value: ProtoValue,
}

fn read_varint(bytes: &[u8], pos: &mut usize) -> Result<u64, String> {
    let mut result: u64 = 0;
    let mut shift = 0;
    loop {
        if *pos >= bytes.len() {
            return Err("Unexpected end of input while reading varint".to_string());
        }
        let byte = bytes[*pos];
        *pos += 1;
        result |= ((byte & 0x7F) as u64) << shift;
        if byte & 0x80 == 0 {
            break;
        }
        shift += 7;
        if shift >= 64 {
            return Err("Varint too long".to_string());
        }
    }
    Ok(result)
}

// Best-effort raw wire-format decode (no .proto schema needed, same
// approach tools like protoscope use). Length-delimited fields are
// speculatively re-decoded as nested messages; ambiguous byte strings
// that happen to parse as valid-looking submessages are an inherent
// limitation of schema-less decoding.
fn decode_fields(bytes: &[u8]) -> Result<Vec<ProtoField>, String> {
    let mut fields = Vec::new();
    let mut pos = 0;
    while pos < bytes.len() {
        let tag = read_varint(bytes, &mut pos)?;
        let field_number = tag >> 3;
        let wire_type = (tag & 0x7) as u8;
        if field_number == 0 {
            return Err("Invalid field number 0".to_string());
        }
        let value = match wire_type {
            0 => ProtoValue::Varint {
                value: read_varint(bytes, &mut pos)?,
            },
            1 => {
                if pos + 8 > bytes.len() {
                    return Err("Unexpected end of input while reading fixed64".to_string());
                }
                let v = u64::from_le_bytes(bytes[pos..pos + 8].try_into().unwrap());
                pos += 8;
                ProtoValue::Fixed64 { value: v }
            }
            2 => {
                let len = read_varint(bytes, &mut pos)? as usize;
                if pos + len > bytes.len() {
                    return Err(
                        "Unexpected end of input while reading length-delimited field".to_string(),
                    );
                }
                let slice = &bytes[pos..pos + len];
                pos += len;
                let text = std::str::from_utf8(slice)
                    .ok()
                    .filter(|s| {
                        !s.is_empty() && s.chars().all(|c| !c.is_control() || c.is_whitespace())
                    })
                    .map(|s| s.to_string());
                let nested = decode_fields(slice).ok().filter(|f| !f.is_empty());
                ProtoValue::Bytes {
                    hex: hex::encode(slice),
                    text,
                    nested,
                }
            }
            5 => {
                if pos + 4 > bytes.len() {
                    return Err("Unexpected end of input while reading fixed32".to_string());
                }
                let v = u32::from_le_bytes(bytes[pos..pos + 4].try_into().unwrap());
                pos += 4;
                ProtoValue::Fixed32 { value: v }
            }
            other => return Err(format!("Unsupported wire type {other}")),
        };
        fields.push(ProtoField {
            field_number,
            wire_type,
            value,
        });
    }
    Ok(fields)
}

fn parse_hex_input(hex: &str) -> Result<Vec<u8>, String> {
    let clean: String = hex.chars().filter(|c| !c.is_whitespace()).collect();
    hex::decode(&clean).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn decode_protobuf(hex: String) -> Result<Vec<ProtoField>, String> {
    let bytes = parse_hex_input(&hex)?;
    decode_fields(&bytes)
}

#[tauri::command]
pub fn decode_msgpack(hex: String) -> Result<serde_json::Value, String> {
    let bytes = parse_hex_input(&hex)?;
    rmp_serde::from_slice::<serde_json::Value>(&bytes).map_err(|e| e.to_string())
}

#[tauri::command]
pub fn read_file_hex(path: String) -> Result<String, String> {
    let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
    Ok(hex::encode(bytes))
}