sim-codec-classfile 0.1.0

Bounded, lossless Java Virtual Machine classfile codec for SIM
Documentation
struct DecodeState {
    budget: ShellBudget,
    attributes: usize,
    attribute_bytes: usize,
    codec: CodecId,
    source: SourceId,
}

struct Member {
    access_flags: u16,
    name_index: u16,
    descriptor_index: u16,
    attributes: Vec<AttributeShell>,
    origin: Origin,
}

impl Member {
    fn into_field(self) -> FieldShell {
        FieldShell {
            access_flags: self.access_flags,
            name_index: self.name_index,
            descriptor_index: self.descriptor_index,
            attributes: self.attributes,
            origin: self.origin,
        }
    }
    fn into_method(self) -> MethodShell {
        MethodShell {
            access_flags: self.access_flags,
            name_index: self.name_index,
            descriptor_index: self.descriptor_index,
            attributes: self.attributes,
            origin: self.origin,
        }
    }
}

fn read_indices(
    reader: &mut ByteReader<'_>,
    limit: usize,
    path: &str,
) -> Result<Vec<u16>, ShellError> {
    let count = usize::from(reader.read_u2().map_err(|e| byte_error(path, e))?);
    check_count(reader, count, limit, path)?;
    (0..count)
        .map(|position| {
            reader
                .read_u2()
                .map_err(|e| byte_error(&format!("{path}[{position}]"), e))
        })
        .collect()
}

fn read_members(
    reader: &mut ByteReader<'_>,
    limit: usize,
    path: &str,
    owner: fn(usize) -> AttributeOwner,
    state: &mut DecodeState,
) -> Result<Vec<Member>, ShellError> {
    let count = usize::from(reader.read_u2().map_err(|e| byte_error(path, e))?);
    check_count(reader, count, limit, path)?;
    let mut members = Vec::with_capacity(count);
    for position in 0..count {
        let member_path = format!("{path}[{position}]");
        let start = reader.offset();
        let access_flags = reader.read_u2().map_err(|e| byte_error(&member_path, e))?;
        let name_index = reader.read_u2().map_err(|e| byte_error(&member_path, e))?;
        let descriptor_index = reader.read_u2().map_err(|e| byte_error(&member_path, e))?;
        let attributes = read_attributes(reader, &member_path, owner(position), state)?;
        members.push(Member {
            access_flags,
            name_index,
            descriptor_index,
            attributes,
            origin: origin(state.codec, state.source.clone(), start, reader.offset()),
        });
    }
    Ok(members)
}

fn read_attributes(
    reader: &mut ByteReader<'_>,
    owner: &str,
    attribute_owner: AttributeOwner,
    state: &mut DecodeState,
) -> Result<Vec<AttributeShell>, ShellError> {
    let count = usize::from(reader.read_u2().map_err(|e| byte_error(owner, e))?);
    state.attributes = state
        .attributes
        .checked_add(count)
        .ok_or_else(|| budget_error(reader.offset(), owner))?;
    check_count(reader, state.attributes, state.budget.attributes, owner)?;
    let mut attributes = Vec::with_capacity(count);
    for position in 0..count {
        let path = format!("{owner}.attributes[{position}]");
        let start = reader.offset();
        let name_index = reader.read_u2().map_err(|e| byte_error(&path, e))?;
        let declared_length = reader.read_u4().map_err(|e| byte_error(&path, e))?;
        let length =
            usize::try_from(declared_length).map_err(|_| budget_error(reader.offset(), &path))?;
        state.attribute_bytes = state
            .attribute_bytes
            .checked_add(length)
            .ok_or_else(|| budget_error(reader.offset(), &path))?;
        check_count(
            reader,
            state.attribute_bytes,
            state.budget.attribute_bytes,
            &path,
        )?;
        let bytes = reader
            .take(length)
            .map_err(|e| byte_error(&path, e))?
            .to_vec();
        attributes.push(AttributeShell {
            name_index,
            declared_length,
            bytes,
            origin: origin(state.codec, state.source.clone(), start, reader.offset()),
            location: AttributeLocation {
                owner: attribute_owner,
                order: position,
            },
        });
    }
    Ok(attributes)
}

trait MemberShell {
    fn header(&self) -> (u16, u16, u16);
    fn attributes(&self) -> &[AttributeShell];
}
impl MemberShell for FieldShell {
    fn header(&self) -> (u16, u16, u16) {
        (self.access_flags, self.name_index, self.descriptor_index)
    }
    fn attributes(&self) -> &[AttributeShell] {
        &self.attributes
    }
}
impl MemberShell for MethodShell {
    fn header(&self) -> (u16, u16, u16) {
        (self.access_flags, self.name_index, self.descriptor_index)
    }
    fn attributes(&self) -> &[AttributeShell] {
        &self.attributes
    }
}
fn write_indices(out: &mut ByteWriter, values: &[u16], path: &str) -> Result<(), ShellError> {
    out.write_u2(u16::try_from(values.len()).map_err(|_| edit_error(path, "count exceeds u16"))?)
        .map_err(|e| byte_error(path, e))?;
    for value in values {
        out.write_u2(*value).map_err(|e| byte_error(path, e))?;
    }
    Ok(())
}
fn write_members<T: MemberShell>(
    out: &mut ByteWriter,
    values: &[T],
    path: &str,
) -> Result<(), ShellError> {
    out.write_u2(u16::try_from(values.len()).map_err(|_| edit_error(path, "count exceeds u16"))?)
        .map_err(|e| byte_error(path, e))?;
    for value in values {
        let (flags, name, descriptor) = value.header();
        out.write_u2(flags).map_err(|e| byte_error(path, e))?;
        out.write_u2(name).map_err(|e| byte_error(path, e))?;
        out.write_u2(descriptor).map_err(|e| byte_error(path, e))?;
        write_attributes(out, value.attributes(), path)?;
    }
    Ok(())
}
fn write_attributes(
    out: &mut ByteWriter,
    values: &[AttributeShell],
    path: &str,
) -> Result<(), ShellError> {
    out.write_u2(
        u16::try_from(values.len()).map_err(|_| edit_error(path, "attribute count exceeds u16"))?,
    )
    .map_err(|e| byte_error(path, e))?;
    for attribute in values {
        if usize::try_from(attribute.declared_length).ok() != Some(attribute.bytes.len()) {
            return Err(edit_error(
                path,
                "declared attribute length differs from retained bytes",
            ));
        }
        out.write_u2(attribute.name_index)
            .map_err(|e| byte_error(path, e))?;
        out.write_u4(attribute.declared_length)
            .map_err(|e| byte_error(path, e))?;
        out.write_bytes(&attribute.bytes)
            .map_err(|e| byte_error(path, e))?;
    }
    Ok(())
}

fn edit_error(path: impl Into<String>, message: impl Into<String>) -> ShellError {
    error(ShellErrorKind::Edit, 0, None, path, message)
}

fn check_count(
    reader: &ByteReader<'_>,
    count: usize,
    limit: usize,
    path: &str,
) -> Result<(), ShellError> {
    if count > limit {
        return Err(budget_error(reader.offset(), path));
    }
    reader
        .preflight_allocation(count)
        .map_err(|_| budget_error(reader.offset(), path))
}

fn origin(codec: CodecId, source: SourceId, start: usize, end: usize) -> Origin {
    Origin {
        codec,
        source,
        span: Span { start, end },
        trivia: Vec::new(),
    }
}

fn byte_error(path: &str, cause: ByteError) -> ShellError {
    error(
        ShellErrorKind::Bytes,
        cause.offset,
        None,
        path,
        cause.to_string(),
    )
}
fn pool_error(cause: ConstantPoolError) -> ShellError {
    error(
        ShellErrorKind::ConstantPool,
        8,
        cause.target_index,
        "constant_pool",
        cause.to_string(),
    )
}
fn budget_error(offset: usize, path: &str) -> ShellError {
    error(
        ShellErrorKind::Budget,
        offset,
        None,
        path,
        "shell decode budget exceeded",
    )
}
fn error(
    kind: ShellErrorKind,
    offset: usize,
    index: Option<u16>,
    path: impl Into<String>,
    message: impl Into<String>,
) -> ShellError {
    ShellError {
        kind,
        offset,
        index,
        path: path.into(),
        message: message.into(),
    }
}