use std::{error::Error, fmt};
pub const NCS_HEADER: &str = "NCS V1.0";
pub const NCS_BINARY_HEADER_SIZE: usize = 13;
pub const NCS_OPERATION_BASE_SIZE: usize = 2;
pub const NCS_OPCODE_OFFSET: usize = 0;
pub const NCS_AUXCODE_OFFSET: usize = 1;
pub const NCS_EXTRA_DATA_OFFSET: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NcsHeader {
pub code_size: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NcsHeaderError {
TooShort(usize),
InvalidMagic,
InvalidMarker(u8),
}
impl fmt::Display for NcsHeaderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooShort(len) => write!(f, "NCS header too short: expected 13 bytes, got {len}"),
Self::InvalidMagic => f.write_str("invalid NCS header magic"),
Self::InvalidMarker(marker) => write!(f, "invalid NCS binary marker: {marker:#04x}"),
}
}
}
impl Error for NcsHeaderError {}
pub fn decode_ncs_header(bytes: &[u8]) -> Result<NcsHeader, NcsHeaderError> {
if bytes.len() < NCS_BINARY_HEADER_SIZE {
return Err(NcsHeaderError::TooShort(bytes.len()));
}
if bytes.get(..NCS_HEADER.len()) != Some(NCS_HEADER.as_bytes()) {
return Err(NcsHeaderError::InvalidMagic);
}
let Some(marker) = bytes.get(8).copied() else {
return Err(NcsHeaderError::TooShort(bytes.len()));
};
if marker != b'B' {
return Err(NcsHeaderError::InvalidMarker(marker));
}
let Some(code_size_bytes) = bytes.get(9..13) else {
return Err(NcsHeaderError::TooShort(bytes.len()));
};
let code_size = <[u8; 4]>::try_from(code_size_bytes)
.map(u32::from_be_bytes)
.map_err(|_error| NcsHeaderError::TooShort(bytes.len()))?;
Ok(NcsHeader {
code_size,
})
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NcsInstruction {
pub opcode: NcsOpcode,
pub auxcode: NcsAuxCode,
pub extra: Vec<u8>,
}
impl NcsInstruction {
#[must_use]
pub fn encoded_len(&self) -> usize {
NCS_OPERATION_BASE_SIZE + self.extra.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum NcsOpcode {
Assignment = 0x01,
RunstackAdd = 0x02,
RunstackCopy = 0x03,
Constant = 0x04,
ExecuteCommand = 0x05,
LogicalAnd = 0x06,
LogicalOr = 0x07,
InclusiveOr = 0x08,
ExclusiveOr = 0x09,
BooleanAnd = 0x0a,
Equal = 0x0b,
NotEqual = 0x0c,
Geq = 0x0d,
Gt = 0x0e,
Lt = 0x0f,
Leq = 0x10,
ShiftLeft = 0x11,
ShiftRight = 0x12,
UShiftRight = 0x13,
Add = 0x14,
Sub = 0x15,
Mul = 0x16,
Div = 0x17,
Modulus = 0x18,
Negation = 0x19,
OnesComplement = 0x1a,
ModifyStackPointer = 0x1b,
StoreIp = 0x1c,
Jmp = 0x1d,
Jsr = 0x1e,
Jz = 0x1f,
Ret = 0x20,
DeStruct = 0x21,
BooleanNot = 0x22,
Decrement = 0x23,
Increment = 0x24,
Jnz = 0x25,
AssignmentBase = 0x26,
RunstackCopyBase = 0x27,
DecrementBase = 0x28,
IncrementBase = 0x29,
SaveBasePointer = 0x2a,
RestoreBasePointer = 0x2b,
StoreState = 0x2c,
NoOperation = 0x2d,
}
impl NcsOpcode {
#[must_use]
pub fn canonical_name(self) -> &'static str {
match self {
Self::Assignment => "CPDOWNSP",
Self::RunstackAdd => "RSADD",
Self::RunstackCopy => "CPTOPSP",
Self::Constant => "CONST",
Self::ExecuteCommand => "ACTION",
Self::LogicalAnd => "LOGAND",
Self::LogicalOr => "LOGOR",
Self::InclusiveOr => "INCOR",
Self::ExclusiveOr => "EXCOR",
Self::BooleanAnd => "BOOLAND",
Self::Equal => "EQUAL",
Self::NotEqual => "NEQUAL",
Self::Geq => "GEQ",
Self::Gt => "GT",
Self::Lt => "LT",
Self::Leq => "LEQ",
Self::ShiftLeft => "SHLEFT",
Self::ShiftRight => "SHRIGHT",
Self::UShiftRight => "USHRIGHT",
Self::Add => "ADD",
Self::Sub => "SUB",
Self::Mul => "MUL",
Self::Div => "DIV",
Self::Modulus => "MOD",
Self::Negation => "NEG",
Self::OnesComplement => "COMP",
Self::ModifyStackPointer => "MOVSP",
Self::StoreIp => "STOREIP",
Self::Jmp => "JMP",
Self::Jsr => "JSR",
Self::Jz => "JZ",
Self::Ret => "RET",
Self::DeStruct => "DESTRUCT",
Self::BooleanNot => "NOT",
Self::Decrement => "DECSP",
Self::Increment => "INCSP",
Self::Jnz => "JNZ",
Self::AssignmentBase => "CPDOWNBP",
Self::RunstackCopyBase => "CPTOPBP",
Self::DecrementBase => "DECBP",
Self::IncrementBase => "INCBP",
Self::SaveBasePointer => "SAVEBP",
Self::RestoreBasePointer => "RESTOREBP",
Self::StoreState => "STORESTATE",
Self::NoOperation => "NOP",
}
}
}
impl fmt::Display for NcsOpcode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.canonical_name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum NcsAuxCode {
None = 0x00,
TypeVoid = 0x01,
TypeCommand = 0x02,
TypeInteger = 0x03,
TypeFloat = 0x04,
TypeString = 0x05,
TypeObject = 0x06,
TypeEngst0 = 0x10,
TypeEngst1 = 0x11,
TypeEngst2 = 0x12,
TypeEngst3 = 0x13,
TypeEngst4 = 0x14,
TypeEngst5 = 0x15,
TypeEngst6 = 0x16,
TypeEngst7 = 0x17,
TypeEngst8 = 0x18,
TypeEngst9 = 0x19,
TypeTypeIntegerInteger = 0x20,
TypeTypeFloatFloat = 0x21,
TypeTypeObjectObject = 0x22,
TypeTypeStringString = 0x23,
TypeTypeStructStruct = 0x24,
TypeTypeIntegerFloat = 0x25,
TypeTypeFloatInteger = 0x26,
TypeTypeEngst0Engst0 = 0x30,
TypeTypeEngst1Engst1 = 0x31,
TypeTypeEngst2Engst2 = 0x32,
TypeTypeEngst3Engst3 = 0x33,
TypeTypeEngst4Engst4 = 0x34,
TypeTypeEngst5Engst5 = 0x35,
TypeTypeEngst6Engst6 = 0x36,
TypeTypeEngst7Engst7 = 0x37,
TypeTypeEngst8Engst8 = 0x38,
TypeTypeEngst9Engst9 = 0x39,
TypeTypeVectorVector = 0x3a,
TypeTypeVectorFloat = 0x3b,
TypeTypeFloatVector = 0x3c,
EvalInplace = 0x70,
EvalPostplace = 0x71,
}
impl NcsAuxCode {
#[must_use]
pub fn canonical_name(self) -> Option<&'static str> {
match self {
Self::None
| Self::TypeVoid
| Self::TypeCommand
| Self::EvalInplace
| Self::EvalPostplace => None,
Self::TypeInteger => Some("I"),
Self::TypeFloat => Some("F"),
Self::TypeString => Some("S"),
Self::TypeObject => Some("O"),
Self::TypeEngst0 => Some("E0"),
Self::TypeEngst1 => Some("E1"),
Self::TypeEngst2 => Some("E2"),
Self::TypeEngst3 => Some("E3"),
Self::TypeEngst4 => Some("E4"),
Self::TypeEngst5 => Some("E5"),
Self::TypeEngst6 => Some("E6"),
Self::TypeEngst7 => Some("E7"),
Self::TypeEngst8 => Some("E8"),
Self::TypeEngst9 => Some("E9"),
Self::TypeTypeIntegerInteger => Some("II"),
Self::TypeTypeFloatFloat => Some("FF"),
Self::TypeTypeObjectObject => Some("OO"),
Self::TypeTypeStringString => Some("SS"),
Self::TypeTypeStructStruct => Some("TT"),
Self::TypeTypeIntegerFloat => Some("IF"),
Self::TypeTypeFloatInteger => Some("FI"),
Self::TypeTypeEngst0Engst0 => Some("E0E0"),
Self::TypeTypeEngst1Engst1 => Some("E1E1"),
Self::TypeTypeEngst2Engst2 => Some("E2E2"),
Self::TypeTypeEngst3Engst3 => Some("E3E3"),
Self::TypeTypeEngst4Engst4 => Some("E4E4"),
Self::TypeTypeEngst5Engst5 => Some("E5E5"),
Self::TypeTypeEngst6Engst6 => Some("E6E6"),
Self::TypeTypeEngst7Engst7 => Some("E7E7"),
Self::TypeTypeEngst8Engst8 => Some("E8E8"),
Self::TypeTypeEngst9Engst9 => Some("E9E9"),
Self::TypeTypeVectorVector => Some("VV"),
Self::TypeTypeVectorFloat => Some("VF"),
Self::TypeTypeFloatVector => Some("FV"),
}
}
}
impl fmt::Display for NcsAuxCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(name) = self.canonical_name() {
f.write_str(name)
} else {
write!(f, "{:#04x}", *self as u8)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownNcsOpcode(pub u8);
impl fmt::Display for UnknownNcsOpcode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown NCS opcode: {:#04x}", self.0)
}
}
impl Error for UnknownNcsOpcode {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownNcsAuxCode(pub u8);
impl fmt::Display for UnknownNcsAuxCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown NCS aux code: {:#04x}", self.0)
}
}
impl Error for UnknownNcsAuxCode {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NcsReadError {
Header(NcsHeaderError),
Opcode(UnknownNcsOpcode),
AuxCode(UnknownNcsAuxCode),
TruncatedInstruction {
offset: usize,
expected_extra: usize,
actual_extra: usize,
},
}
impl fmt::Display for NcsReadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Header(error) => error.fmt(f),
Self::Opcode(error) => error.fmt(f),
Self::AuxCode(error) => error.fmt(f),
Self::TruncatedInstruction {
offset,
expected_extra,
actual_extra,
} => write!(
f,
"truncated NCS instruction at byte {offset}: expected {expected_extra} payload \
bytes, got {actual_extra}"
),
}
}
}
impl Error for NcsReadError {}
impl From<NcsHeaderError> for NcsReadError {
fn from(value: NcsHeaderError) -> Self {
Self::Header(value)
}
}
impl From<UnknownNcsOpcode> for NcsReadError {
fn from(value: UnknownNcsOpcode) -> Self {
Self::Opcode(value)
}
}
impl From<UnknownNcsAuxCode> for NcsReadError {
fn from(value: UnknownNcsAuxCode) -> Self {
Self::AuxCode(value)
}
}
impl TryFrom<u8> for NcsOpcode {
type Error = UnknownNcsOpcode;
fn try_from(value: u8) -> Result<Self, Self::Error> {
let opcode = match value {
0x01 => Self::Assignment,
0x02 => Self::RunstackAdd,
0x03 => Self::RunstackCopy,
0x04 => Self::Constant,
0x05 => Self::ExecuteCommand,
0x06 => Self::LogicalAnd,
0x07 => Self::LogicalOr,
0x08 => Self::InclusiveOr,
0x09 => Self::ExclusiveOr,
0x0a => Self::BooleanAnd,
0x0b => Self::Equal,
0x0c => Self::NotEqual,
0x0d => Self::Geq,
0x0e => Self::Gt,
0x0f => Self::Lt,
0x10 => Self::Leq,
0x11 => Self::ShiftLeft,
0x12 => Self::ShiftRight,
0x13 => Self::UShiftRight,
0x14 => Self::Add,
0x15 => Self::Sub,
0x16 => Self::Mul,
0x17 => Self::Div,
0x18 => Self::Modulus,
0x19 => Self::Negation,
0x1a => Self::OnesComplement,
0x1b => Self::ModifyStackPointer,
0x1c => Self::StoreIp,
0x1d => Self::Jmp,
0x1e => Self::Jsr,
0x1f => Self::Jz,
0x20 => Self::Ret,
0x21 => Self::DeStruct,
0x22 => Self::BooleanNot,
0x23 => Self::Decrement,
0x24 => Self::Increment,
0x25 => Self::Jnz,
0x26 => Self::AssignmentBase,
0x27 => Self::RunstackCopyBase,
0x28 => Self::DecrementBase,
0x29 => Self::IncrementBase,
0x2a => Self::SaveBasePointer,
0x2b => Self::RestoreBasePointer,
0x2c => Self::StoreState,
0x2d => Self::NoOperation,
_ => return Err(UnknownNcsOpcode(value)),
};
Ok(opcode)
}
}
impl TryFrom<u8> for NcsAuxCode {
type Error = UnknownNcsAuxCode;
fn try_from(value: u8) -> Result<Self, Self::Error> {
let aux = match value {
0x00 => Self::None,
0x01 => Self::TypeVoid,
0x02 => Self::TypeCommand,
0x03 => Self::TypeInteger,
0x04 => Self::TypeFloat,
0x05 => Self::TypeString,
0x06 => Self::TypeObject,
0x10 => Self::TypeEngst0,
0x11 => Self::TypeEngst1,
0x12 => Self::TypeEngst2,
0x13 => Self::TypeEngst3,
0x14 => Self::TypeEngst4,
0x15 => Self::TypeEngst5,
0x16 => Self::TypeEngst6,
0x17 => Self::TypeEngst7,
0x18 => Self::TypeEngst8,
0x19 => Self::TypeEngst9,
0x20 => Self::TypeTypeIntegerInteger,
0x21 => Self::TypeTypeFloatFloat,
0x22 => Self::TypeTypeObjectObject,
0x23 => Self::TypeTypeStringString,
0x24 => Self::TypeTypeStructStruct,
0x25 => Self::TypeTypeIntegerFloat,
0x26 => Self::TypeTypeFloatInteger,
0x30 => Self::TypeTypeEngst0Engst0,
0x31 => Self::TypeTypeEngst1Engst1,
0x32 => Self::TypeTypeEngst2Engst2,
0x33 => Self::TypeTypeEngst3Engst3,
0x34 => Self::TypeTypeEngst4Engst4,
0x35 => Self::TypeTypeEngst5Engst5,
0x36 => Self::TypeTypeEngst6Engst6,
0x37 => Self::TypeTypeEngst7Engst7,
0x38 => Self::TypeTypeEngst8Engst8,
0x39 => Self::TypeTypeEngst9Engst9,
0x3a => Self::TypeTypeVectorVector,
0x3b => Self::TypeTypeVectorFloat,
0x3c => Self::TypeTypeFloatVector,
0x70 => Self::EvalInplace,
0x71 => Self::EvalPostplace,
_ => return Err(UnknownNcsAuxCode(value)),
};
Ok(aux)
}
}
fn instruction_extra_size(opcode: NcsOpcode, auxcode: NcsAuxCode, bytes: &[u8]) -> usize {
match opcode {
NcsOpcode::Constant => match auxcode {
NcsAuxCode::TypeInteger
| NcsAuxCode::TypeFloat
| NcsAuxCode::TypeObject
| NcsAuxCode::TypeEngst2 => 4,
NcsAuxCode::TypeString | NcsAuxCode::TypeEngst7 => {
match bytes
.get(..2)
.and_then(|prefix| <[u8; 2]>::try_from(prefix).ok())
{
Some(prefix) => 2 + usize::from(u16::from_be_bytes(prefix)),
None => 2,
}
}
_ => 0,
},
NcsOpcode::Jmp
| NcsOpcode::Jsr
| NcsOpcode::Jz
| NcsOpcode::Jnz
| NcsOpcode::ModifyStackPointer
| NcsOpcode::Decrement
| NcsOpcode::Increment
| NcsOpcode::DecrementBase
| NcsOpcode::IncrementBase => 4,
NcsOpcode::StoreState => 8,
NcsOpcode::ExecuteCommand => 3,
NcsOpcode::RunstackCopy
| NcsOpcode::RunstackCopyBase
| NcsOpcode::Assignment
| NcsOpcode::AssignmentBase
| NcsOpcode::DeStruct => 6,
NcsOpcode::Equal | NcsOpcode::NotEqual if auxcode == NcsAuxCode::TypeTypeStructStruct => 2,
_ => 0,
}
}
pub fn decode_ncs_instructions(bytes: &[u8]) -> Result<Vec<NcsInstruction>, NcsReadError> {
let header = decode_ncs_header(bytes)?;
let mut offset = NCS_BINARY_HEADER_SIZE;
let code_end = NCS_BINARY_HEADER_SIZE + header.code_size as usize;
if bytes.len() < code_end {
return Err(NcsReadError::TruncatedInstruction {
offset,
expected_extra: header.code_size as usize,
actual_extra: bytes.len().saturating_sub(offset),
});
}
let mut instructions = Vec::new();
while offset < code_end {
let opcode = NcsOpcode::try_from(*bytes.get(offset).ok_or(
NcsReadError::TruncatedInstruction {
offset,
expected_extra: 1,
actual_extra: bytes.len().saturating_sub(offset),
},
)?)?;
let auxcode = NcsAuxCode::try_from(*bytes.get(offset + 1).ok_or(
NcsReadError::TruncatedInstruction {
offset,
expected_extra: 2,
actual_extra: bytes.len().saturating_sub(offset),
},
)?)?;
let extra_window = bytes.get(offset + 2..code_end).unwrap_or(&[]);
let extra_size = instruction_extra_size(opcode, auxcode, extra_window);
let remaining = code_end.saturating_sub(offset + 2);
if remaining < extra_size {
return Err(NcsReadError::TruncatedInstruction {
offset,
expected_extra: extra_size,
actual_extra: remaining,
});
}
let extra = bytes
.get(offset + 2..offset + 2 + extra_size)
.unwrap_or(&[])
.to_vec();
instructions.push(NcsInstruction {
opcode,
auxcode,
extra,
});
offset += NCS_OPERATION_BASE_SIZE + extra_size;
}
Ok(instructions)
}
pub fn encode_ncs_instructions(instructions: &[NcsInstruction]) -> Vec<u8> {
let code_size = u32::try_from(
instructions
.iter()
.map(NcsInstruction::encoded_len)
.sum::<usize>(),
)
.ok()
.unwrap_or(u32::MAX);
let mut bytes = Vec::with_capacity(
NCS_BINARY_HEADER_SIZE + usize::try_from(code_size).ok().unwrap_or(usize::MAX),
);
bytes.extend_from_slice(NCS_HEADER.as_bytes());
bytes.push(b'B');
bytes.extend_from_slice(&code_size.to_be_bytes());
for instruction in instructions {
bytes.push(instruction.opcode as u8);
bytes.push(instruction.auxcode as u8);
bytes.extend_from_slice(&instruction.extra);
}
bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_header_accepts_valid_prefix() -> Result<(), Box<dyn std::error::Error>> {
let bytes = [
b'N', b'C', b'S', b' ', b'V', b'1', b'.', b'0', b'B', 0x00, 0x00, 0x00, 0x2a,
];
let header = decode_ncs_header(&bytes)?;
assert_eq!(header.code_size, 42);
Ok(())
}
#[test]
fn decode_header_rejects_bad_marker() {
let bytes = [
b'N', b'C', b'S', b' ', b'V', b'1', b'.', b'0', b'X', 0x00, 0x00, 0x00, 0x2a,
];
assert_eq!(
decode_ncs_header(&bytes),
Err(NcsHeaderError::InvalidMarker(b'X'))
);
}
#[test]
fn encode_and_decode_roundtrip_instruction_stream() -> Result<(), Box<dyn std::error::Error>> {
let instructions = vec![
NcsInstruction {
opcode: NcsOpcode::Constant,
auxcode: NcsAuxCode::TypeInteger,
extra: 42_i32.to_be_bytes().to_vec(),
},
NcsInstruction {
opcode: NcsOpcode::Ret,
auxcode: NcsAuxCode::None,
extra: Vec::new(),
},
];
let bytes = encode_ncs_instructions(&instructions);
let decoded = decode_ncs_instructions(&bytes)?;
assert_eq!(decoded, instructions);
Ok(())
}
}