Skip to content

Instantly share code, notes, and snippets.

@randrews
Last active December 9, 2025 05:31
Show Gist options
  • Select an option

  • Save randrews/bef62954159b07341912e1b610ee3943 to your computer and use it in GitHub Desktop.

Select an option

Save randrews/bef62954159b07341912e1b610ee3943 to your computer and use it in GitHub Desktop.
use std::collections::VecDeque;
#[derive(Copy, Clone, Debug, PartialEq)]
#[rustfmt::skip]
enum Alphabet {
L, U, P,
}
enum ZChar {
Literal(char),
Abbreviation(u8, u8),
}
#[rustfmt::skip]
const EXTENDED_CHARS: [char; 69] = [
'\u{00e4}', '\u{00f6}', '\u{00fc}', '\u{00c4}', '\u{00d6}', '\u{00dc}', '\u{00df}', '\u{00bb}',
'\u{00ab}', '\u{00eb}', '\u{00ef}', '\u{00ff}', '\u{00cb}', '\u{00cf}', '\u{00e1}', '\u{00e9}',
'\u{00ed}', '\u{00f3}', '\u{00fa}', '\u{00fd}', '\u{00c1}', '\u{00c9}', '\u{00cd}', '\u{00d3}',
'\u{00da}', '\u{00dd}', '\u{00e0}', '\u{00e8}', '\u{00ec}', '\u{00f2}', '\u{00f9}', '\u{00c0}',
'\u{00c8}', '\u{00cc}', '\u{00d2}', '\u{00d9}', '\u{00e2}', '\u{00ea}', '\u{00ee}', '\u{00f4}',
'\u{00fb}', '\u{00c2}', '\u{00ca}', '\u{00ce}', '\u{00d4}', '\u{00db}', '\u{00e5}', '\u{00c5}',
'\u{00f8}', '\u{00d8}', '\u{00e3}', '\u{00f1}', '\u{00f5}', '\u{00c3}', '\u{00d1}', '\u{00d5}',
'\u{00e6}', '\u{00c6}', '\u{00e7}', '\u{00c7}', '\u{00fe}', '\u{00f0}', '\u{00de}', '\u{00d0}',
'\u{00a3}', '\u{0153}', '\u{0152}', '\u{00a1}', '\u{00bf}',
];
impl ZChar {
fn parse<I: Iterator<Item = u8>>(iter: &mut I) -> Option<Self> {
let mut zch = iter.next()?;
let mut alphabet = Alphabet::L;
loop {
match zch & 0x1f {
// 0 is always a space
0 => return Some(ZChar::Literal(' ')),
// 1-3 is an abbreviation, which depends on the current alphabet
1..4 => return Some(ZChar::Abbreviation(zch, iter.next()? & 0x1f)),
// 4 and 5 change alphabets for the next char:
4 => {
alphabet = Alphabet::U;
zch = iter.next()?
}
5 => {
alphabet = Alphabet::P;
zch = iter.next()?
}
// 6 in alphabet P is a literal:
6 if alphabet == Alphabet::P => {
let high = iter.next()?;
let low = iter.next()?;
return Self::literal(high, low);
}
6..32 => {
let chars = match alphabet {
Alphabet::L => " ^^^^^abcdefghijklmnopqrstuvwxyz",
Alphabet::U => " ^^^^^ABCDEFGHIJKLMNOPQRSTUVWXYZ",
Alphabet::P => " ^^^^^ \n0123456789.,!?_#'\"/\\-:()",
};
let ch = chars
.chars()
.nth(zch as usize)
.expect("We covered everything else above...");
return Some(ZChar::Literal(ch));
}
_ => unreachable!(),
}
}
}
fn literal(high: u8, low: u8) -> Option<Self> {
let value = ((high as u16) & 0x1f) << 5 | ((low as u16) & 0x1f);
match value {
0 => Some(Self::Literal('\0')),
13 => Some(Self::Literal('\n')),
32..127 => Some(Self::Literal(char::from_u32(value as u32).unwrap())),
155..252 => Some(Self::Literal(EXTENDED_CHARS[(value - 155) as usize])),
_ => None,
}
}
}
pub fn convert(zscii: &[u8]) -> String {
let mut zchars = VecDeque::new();
let mut i = 0usize;
loop {
// This can't happen because we'll search for the terminating
// word before sending a slice here
assert!(i < zscii.len());
let word = (zscii[i] as u16) << 8 | zscii[i + 1] as u16;
zchars.push_back(((word >> 10) & 0x1f) as u8);
zchars.push_back(((word >> 5) & 0x1f) as u8);
zchars.push_back((word & 0x1f) as u8);
if word & 0x8000 != 0 {
break;
}
i += 2;
}
let mut output = String::new();
let mut iter = zchars.into_iter();
while let Some(zch) = ZChar::parse(&mut iter) {
match zch {
ZChar::Literal(ch) => output.push(ch),
ZChar::Abbreviation(_, _) => {} // TODO abbreviations
}
}
output
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment