entropy-auth 2026.7.31

Authentication and authorization for Entropy Softworks server and API projects
//! Minimal BER (Basic Encoding Rules) codec — the subset LDAP v3 uses
//! (RFC 4511 §5.1: a restricted BER). Definite-length only; no indefinite
//! form (LDAP forbids it).
//!
//! A TLV is `tag(1 byte) · length · content`. This module reads/writes the
//! primitives LDAP needs — INTEGER, ENUMERATED, BOOLEAN, OCTET STRING, and
//! constructed SEQUENCE / tagged bodies — leaving the LDAP message grammar to
//! [`super::message`].
#![allow(clippy::doc_markdown, clippy::cast_possible_truncation)]

/// A BER decode error (non-secret).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BerError(pub &'static str);

impl BerError {
    /// Whether this error means "the outer frame is truncated — read more
    /// bytes", as opposed to a complete-but-malformed PDU. A caller framing a
    /// stream uses this to keep reading on truncation but reject (and stop) on
    /// a malformed frame, instead of buffering forever on garbage.
    #[must_use]
    pub fn is_incomplete(&self) -> bool {
        matches!(
            self.0,
            "empty" | "missing length" | "truncated length" | "truncated content"
        )
    }

    /// Reclassify an error raised while decoding the BODY of an already-complete
    /// outer frame: a truncation there means a malformed inner child (the outer
    /// length was fully present), NOT "need more bytes", so force
    /// [`Self::is_incomplete`] to `false`. Non-truncation errors pass through
    /// unchanged (they were already malformed).
    #[must_use]
    pub fn into_inner_malformed(self) -> BerError {
        if self.is_incomplete() {
            BerError("malformed inner TLV")
        } else {
            self
        }
    }
}

impl core::fmt::Display for BerError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "BER error: {}", self.0)
    }
}

impl std::error::Error for BerError {}

/// Parse one TLV from the front of `buf`, returning `(tag, content, rest)`.
///
/// # Errors
///
/// [`BerError`] on truncation, an indefinite length, or an oversize length.
pub fn parse_tlv(buf: &[u8]) -> Result<(u8, &[u8], &[u8]), BerError> {
    let tag = *buf.first().ok_or(BerError("empty"))?;
    let len_byte = *buf.get(1).ok_or(BerError("missing length"))?;
    let (len, header) = if len_byte & 0x80 == 0 {
        // Short form: the byte IS the length.
        (len_byte as usize, 2)
    } else {
        let n = (len_byte & 0x7f) as usize;
        if n == 0 {
            return Err(BerError("indefinite length not allowed"));
        }
        if n > 4 {
            return Err(BerError("length too large"));
        }
        let mut len = 0usize;
        for i in 0..n {
            let b = *buf.get(2 + i).ok_or(BerError("truncated length"))?;
            len = (len << 8) | b as usize;
        }
        (len, 2 + n)
    };
    let end = header.checked_add(len).ok_or(BerError("length overflow"))?;
    if end > buf.len() {
        return Err(BerError("truncated content"));
    }
    Ok((tag, &buf[header..end], &buf[end..]))
}

/// Parse a signed INTEGER / ENUMERATED content (big-endian two's complement).
///
/// # Errors
///
/// [`BerError`] if empty or wider than 8 bytes.
pub fn parse_integer(content: &[u8]) -> Result<i64, BerError> {
    if content.is_empty() || content.len() > 8 {
        return Err(BerError("bad integer width"));
    }
    // Sign-extend from the top bit of the first byte.
    let mut value: i64 = if content[0] & 0x80 != 0 { -1 } else { 0 };
    for &b in content {
        value = (value << 8) | i64::from(b);
    }
    Ok(value)
}

/// Parse a BOOLEAN content (any non-zero byte is true, per BER decode leniency).
///
/// # Errors
///
/// [`BerError`] if not exactly one byte.
pub fn parse_bool(content: &[u8]) -> Result<bool, BerError> {
    match content {
        [b] => Ok(*b != 0),
        _ => Err(BerError("bad boolean")),
    }
}

/// Iterate the child TLVs of a constructed value's content.
pub struct Children<'a> {
    rest: &'a [u8],
}

impl<'a> Children<'a> {
    /// Iterate the child TLVs of a constructed value's `content`.
    #[must_use]
    pub fn new(content: &'a [u8]) -> Self {
        Self { rest: content }
    }
}

impl<'a> Iterator for Children<'a> {
    type Item = Result<(u8, &'a [u8]), BerError>;
    fn next(&mut self) -> Option<Self::Item> {
        if self.rest.is_empty() {
            return None;
        }
        match parse_tlv(self.rest) {
            Ok((tag, content, rest)) => {
                self.rest = rest;
                Some(Ok((tag, content)))
            }
            Err(e) => {
                self.rest = &[];
                Some(Err(e))
            }
        }
    }
}

// ── Encoding ────────────────────────────────────────────────────────────────

/// Encode a definite length (minimal form).
fn encode_len(len: usize) -> Vec<u8> {
    if len < 0x80 {
        vec![len as u8]
    } else {
        let mut bytes = Vec::new();
        let mut n = len;
        while n > 0 {
            bytes.insert(0, (n & 0xff) as u8);
            n >>= 8;
        }
        let mut out = vec![0x80 | (bytes.len() as u8)];
        out.extend_from_slice(&bytes);
        out
    }
}

/// Wrap `content` in a TLV with the given `tag`.
#[must_use]
pub fn tlv(tag: u8, content: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(content.len() + 4);
    out.push(tag);
    out.extend_from_slice(&encode_len(content.len()));
    out.extend_from_slice(content);
    out
}

/// Minimal two's-complement content bytes for an integer.
#[must_use]
pub fn integer_content(value: i64) -> Vec<u8> {
    if value == 0 {
        return vec![0];
    }
    let be = value.to_be_bytes();
    // Trim redundant leading 0x00 (positive) / 0xFF (negative) bytes while
    // keeping the sign bit correct.
    let mut start = 0;
    while start < 7 {
        let b = be[start];
        let next = be[start + 1];
        let redundant = (b == 0x00 && next & 0x80 == 0) || (b == 0xff && next & 0x80 != 0);
        if redundant {
            start += 1;
        } else {
            break;
        }
    }
    be[start..].to_vec()
}

/// Encode an INTEGER (`0x02`).
#[must_use]
pub fn encode_integer(value: i64) -> Vec<u8> {
    tlv(0x02, &integer_content(value))
}

/// Encode an ENUMERATED (`0x0A`).
#[must_use]
pub fn encode_enumerated(value: i64) -> Vec<u8> {
    tlv(0x0a, &integer_content(value))
}

/// Encode an OCTET STRING (`0x04`).
#[must_use]
pub fn encode_octet_string(s: &[u8]) -> Vec<u8> {
    tlv(0x04, s)
}

/// Encode a SEQUENCE (`0x30`) from already-encoded children.
#[must_use]
pub fn encode_sequence(children: &[Vec<u8>]) -> Vec<u8> {
    let content: Vec<u8> = children.concat();
    tlv(0x30, &content)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn round_trips_integer() {
        for v in [0i64, 1, 127, 128, 255, 256, -1, -128, 65_535, 2_147_483_647] {
            let enc = encode_integer(v);
            let (tag, content, rest) = parse_tlv(&enc).unwrap();
            assert_eq!(tag, 0x02);
            assert!(rest.is_empty());
            assert_eq!(parse_integer(content).unwrap(), v);
        }
    }

    #[test]
    fn short_and_long_lengths() {
        // Short form (< 128).
        let s = encode_octet_string(b"hi");
        assert_eq!(s, vec![0x04, 0x02, b'h', b'i']);
        // Long form (>= 128): 200-byte string → 0x81 0xC8.
        let big = vec![0u8; 200];
        let enc = encode_octet_string(&big);
        assert_eq!(&enc[..2], &[0x04, 0x81]);
        assert_eq!(enc[2], 200);
        let (tag, content, _) = parse_tlv(&enc).unwrap();
        assert_eq!(tag, 0x04);
        assert_eq!(content.len(), 200);
    }

    #[test]
    fn sequence_children_iterate() {
        let seq = encode_sequence(&[encode_integer(5), encode_octet_string(b"x")]);
        let (tag, content, _) = parse_tlv(&seq).unwrap();
        assert_eq!(tag, 0x30);
        let kids: Vec<_> = Children::new(content).map(Result::unwrap).collect();
        assert_eq!(kids.len(), 2);
        assert_eq!(kids[0].0, 0x02);
        assert_eq!(parse_integer(kids[0].1).unwrap(), 5);
        assert_eq!(kids[1].0, 0x04);
        assert_eq!(kids[1].1, b"x");
    }

    #[test]
    fn rejects_truncated_and_indefinite() {
        assert!(parse_tlv(&[0x04, 0x05, b'a']).is_err()); // says 5, has 1
        assert!(parse_tlv(&[0x30, 0x80]).is_err()); // indefinite length
        assert!(parse_tlv(&[]).is_err());
    }

    #[test]
    fn bool_parse() {
        assert!(parse_bool(&[0xff]).unwrap());
        assert!(!parse_bool(&[0x00]).unwrap());
        assert!(parse_bool(&[0x00, 0x00]).is_err());
    }
}