//! Leser fuer den tokenisierten BASIC-Codeteil binaerer VBDOS-Formulare. use crate::frm::{decode_cp437, error, read_u16, FormError}; fn decode_pcode_bytes(bytes: &[u8]) -> String { bytes .iter() .map(|byte| { if *byte < 0x80 { char::from(*byte) } else { decode_cp437(&[*byte]).chars().next().unwrap() } }) .collect() } fn letter_ranges(mask: u32) -> String { let mut ranges = Vec::new(); let mut first = None; for index in 0..=26 { let set = index < 26 && mask & (1 << (31 - index)) != 0; if set && first.is_none() { first = Some(index); } else if !set && first.is_some() { let start = first.take().unwrap(); let mut range = char::from(b'A' + start as u8).to_string(); if index - start > 1 { range.push('-'); range.push(char::from(b'A' + index as u8 - 1)); } ranges.push(range); } } ranges.join(", ") } #[derive(Clone, Copy)] struct TokenDef { len: i16, rules: &'static [&'static str], } fn token_def(code: u16) -> Option { match code { 0x000 => Some(TokenDef { len: 0, rules: &["newline::=.{#newline}{#tabh}"], }), 0x001 => Some(TokenDef { len: 2, rules: &["newline::=.{#newline}{#tabi}"], }), 0x002 => Some(TokenDef { len: 2, rules: &["newline::=.{#newline-include}"], }), 0x003 => Some(TokenDef { len: 4, rules: &["newline::=.{#newline-include}{#indent:2} "], }), 0x004 => Some(TokenDef { len: 4, rules: &[".{#newline}{#thaddr:0}{#label:2}"], }), 0x005 => Some(TokenDef { len: 6, rules: &[".{#newline}{#thaddr:0}{#label:2} {#indent:4}"], }), 0x006 => Some(TokenDef { len: 0, rules: &[": "], }), 0x007 => Some(TokenDef { len: 2, rules: &[":{#tabi}"], }), 0x008 => Some(TokenDef { len: 0, rules: &["."], }), 0x009 => Some(TokenDef { len: 0, rules: &[""], }), 0x00a => Some(TokenDef { len: -1, rules: &["{#raw:2}"], }), 0x00b => Some(TokenDef { len: 2, rules: &["expr::={#id+}"], }), 0x00c => Some(TokenDef { len: 2, rules: &[ "consts::={const:1} {#id+} = {0}", "consts::={consts:1}, {#id+} = {0}", "{#id+} = {0}", ], }), 0x00d => Some(TokenDef { len: 2, rules: &[ "decls::={decls:1}, {#id+:0} {astype:0}", "decls::={decls:0}, {#id+:0}", "decls::={decl:1} {#id+:0} {astype:0}", "decls::={decl:0} {#id+:0}", "{#id+:0} {astype:0}", "{#id+:0}", ], }), 0x00e => Some(TokenDef { len: 4, rules: &["expr::={##id(expr)}"], }), 0x00f => Some(TokenDef { len: 4, rules: &["{##id(expr)} = {$+0}"], }), 0x010 => Some(TokenDef { len: 4, rules: &["decls::={##id(decl)}"], }), 0x011 => Some(TokenDef { len: 2, rules: &["expr::={0}.{#id}"], }), 0x012 => Some(TokenDef { len: 2, rules: &["{0}.{#id} = {1}"], }), 0x013 => Some(TokenDef { len: 4, rules: &["expr::={0}.{#id:2}({1})"], }), 0x014 => Some(TokenDef { len: 4, rules: &["{0}.{#id:2}({1}) = {2}"], }), 0x015 => Some(TokenDef { len: 4, rules: &["astype::={#tabi:2}AS {#type:0}"], }), 0x016 => Some(TokenDef { len: 4, rules: &["astype::={#tabi:2}AS {#type:0}"], }), 0x017 => Some(TokenDef { len: 0, rules: &[""], }), 0x018 => Some(TokenDef { len: 0, rules: &[""], }), 0x019 => Some(TokenDef { len: 2, rules: &["{#id}"], }), 0x01a => Some(TokenDef { len: 0, rules: &["declmod::=SHARED"], }), 0x01b => Some(TokenDef { len: 6, rules: &["deftype::={#thaddr:0}{#DEFxxx}"], }), 0x01c => Some(TokenDef { len: 0, rules: &["{self:1}, {0}", "REDIM {declmod:1} {0}", "REDIM {0}"], }), 0x01d => Some(TokenDef { len: 2, rules: &["END TYPE"], }), 0x01e => Some(TokenDef { len: 2, rules: &["decl::=SHARED"], }), 0x01f => Some(TokenDef { len: 2, rules: &["decl::=STATIC"], }), 0x020 => Some(TokenDef { len: 4, rules: &["TYPE {#id:2}"], }), 0x021 => Some(TokenDef { len: -1, rules: &["$STATIC{#raw}"], }), 0x022 => Some(TokenDef { len: -1, rules: &["$DYNAMIC{#raw}"], }), 0x023 => Some(TokenDef { len: 0, rules: &["const::=CONST"], }), 0x024 => Some(TokenDef { len: 0, rules: &[""], }), 0x025 => Some(TokenDef { len: 0, rules: &["BYVAL {0}"], }), 0x026 => Some(TokenDef { len: -1, rules: &["{deffn:1} = {0}"], }), 0x027 => Some(TokenDef { len: 0, rules: &["COM({0})"], }), 0x028 => Some(TokenDef { len: 2, rules: &["ON {0} GOSUB {#id}"], }), 0x029 => Some(TokenDef { len: 0, rules: &["KEY({0})"], }), 0x02a => Some(TokenDef { len: 0, rules: &["{0} OFF"], }), 0x02b => Some(TokenDef { len: 0, rules: &["{0} ON"], }), 0x02c => Some(TokenDef { len: 0, rules: &["{0} STOP"], }), 0x02d => Some(TokenDef { len: 0, rules: &["PEN"], }), 0x02e => Some(TokenDef { len: 0, rules: &["PLAY"], }), 0x02f => Some(TokenDef { len: 0, rules: &["PLAY({0})"], }), 0x030 => Some(TokenDef { len: 0, rules: &["SIGNAL({0})"], }), 0x031 => Some(TokenDef { len: 0, rules: &["STRIG({0})"], }), 0x032 => Some(TokenDef { len: 0, rules: &["TIMER"], }), 0x033 => Some(TokenDef { len: 0, rules: &["TIMER({0})"], }), 0x034 => Some(TokenDef { len: 4, rules: &["newline::={#thaddr:0}{#label:2} "], }), 0x035 => Some(TokenDef { len: 6, rules: &["newline::={#thaddr:0}{#label:2} {#indent:4}"], }), 0x037 => Some(TokenDef { len: 4, rules: &["CALL {#id:2}{##call()}"], }), 0x038 => Some(TokenDef { len: 4, rules: &["{#id:2}{##call}"], }), 0x039 => Some(TokenDef { len: 4, rules: &["CALLS {#id:2}{##call()}"], }), 0x03a => Some(TokenDef { len: 0, rules: &["CASE ELSE"], }), 0x03b => Some(TokenDef { len: 0, rules: &["case::={case:1}, {0}", "case::=CASE {0}"], }), 0x03c => Some(TokenDef { len: 0, rules: &["case::={case:2}, {1} TO {0}", "case::=CASE {1} TO {0}"], }), 0x03d => Some(TokenDef { len: 0, rules: &["case::={case:1}, IS = {0}", "case::=CASE IS = {0}"], }), 0x03e => Some(TokenDef { len: 0, rules: &["case::={case:1}, IS < {0}", "case::=CASE IS < {0}"], }), 0x03f => Some(TokenDef { len: 0, rules: &["case::={case:1}, IS > {0}", "case::=CASE IS > {0}"], }), 0x040 => Some(TokenDef { len: 0, rules: &["case::={case:1}, IS <= {0}", "case::=CASE IS <= {0}"], }), 0x041 => Some(TokenDef { len: 0, rules: &["case::={case:1}, IS >= {0}", "case::=CASE IS >= {0}"], }), 0x042 => Some(TokenDef { len: 0, rules: &["case::={case:1}, IS <> {0}", "case::=CASE IS <> {0}"], }), 0x043 => Some(TokenDef { len: 0, rules: &["ON"], }), 0x044 => Some(TokenDef { len: -1, rules: &["DECLARE {#procdecl()}"], }), 0x045 => Some(TokenDef { len: -1, rules: &["deffn::={#procdecl:2}"], }), 0x046 => Some(TokenDef { len: 0, rules: &["DO"], }), 0x047 => Some(TokenDef { len: 0, rules: &["DO UNTIL {0}"], }), 0x048 => Some(TokenDef { len: 2, rules: &["DO WHILE {0}"], }), 0x049 => Some(TokenDef { len: 2, rules: &["{newline:0}ELSE ", " ELSE "], }), 0x04a => Some(TokenDef { len: 2, rules: &["{#id}"], }), 0x04c => Some(TokenDef { len: 0, rules: &[" ELSE "], }), 0x04d => Some(TokenDef { len: 2, rules: &["ELSEIF {0} THEN"], }), 0x04e => Some(TokenDef { len: 0, rules: &["END"], }), 0x04f => Some(TokenDef { len: -1, rules: &["END DEF"], }), 0x050 => Some(TokenDef { len: 0, rules: &["END IF"], }), 0x051 => Some(TokenDef { len: 0, rules: &["END {#proctype}"], }), 0x052 => Some(TokenDef { len: 0, rules: &["END SELECT"], }), 0x053 => Some(TokenDef { len: 2, rules: &["EXIT DO"], }), 0x054 => Some(TokenDef { len: 2, rules: &["EXIT FOR"], }), 0x055 => Some(TokenDef { len: 2, rules: &["EXIT {#proctype}"], }), 0x056 => Some(TokenDef { len: 4, rules: &["FOR {2} = {1} TO {0}"], }), 0x057 => Some(TokenDef { len: 4, rules: &["FOR {3} = {2} TO {1} STEP {0}"], }), 0x058 => Some(TokenDef { len: -1, rules: &["funcdef::={#procdecl}"], }), 0x059 => Some(TokenDef { len: 2, rules: &["GOSUB {#id}"], }), 0x05b => Some(TokenDef { len: 2, rules: &["GOTO {#id}"], }), 0x05d => Some(TokenDef { len: 2, rules: &["IF {0} THEN "], }), 0x05e => Some(TokenDef { len: 2, rules: &["IF {0} THEN {#id}"], }), 0x060 => Some(TokenDef { len: 2, rules: &["IF {0} GOTO {#id}"], }), 0x061 => Some(TokenDef { len: 2, rules: &["IF {0} THEN"], }), 0x062 => Some(TokenDef { len: 2, rules: &["LOOP"], }), 0x063 => Some(TokenDef { len: 2, rules: &["LOOP UNTIL {0}"], }), 0x064 => Some(TokenDef { len: 2, rules: &["LOOP WHILE {0}"], }), 0x065 => Some(TokenDef { len: 4, rules: &["NEXT"], }), 0x066 => Some(TokenDef { len: 4, rules: &["{self:1}, {0}", "NEXT {0}"], }), 0x067 => Some(TokenDef { len: 2, rules: &["ON ERROR GOTO {#id}"], }), 0x068 => Some(TokenDef { len: -1, rules: &["ON {0} GOSUB {#id-list}"], }), 0x069 => Some(TokenDef { len: -1, rules: &["ON {0} GOTO {#id-list}"], }), 0x06a => Some(TokenDef { len: 0, rules: &["RESTORE"], }), 0x06b => Some(TokenDef { len: 2, rules: &["RESTORE {#id}"], }), 0x06c => Some(TokenDef { len: 0, rules: &["RESUME"], }), 0x06d => Some(TokenDef { len: 2, rules: &["RESUME {#id}"], }), 0x06e => Some(TokenDef { len: 0, rules: &["RESUME NEXT"], }), 0x06f => Some(TokenDef { len: 0, rules: &["RETURN"], }), 0x070 => Some(TokenDef { len: 2, rules: &["RETURN {#id}"], }), 0x071 => Some(TokenDef { len: 0, rules: &["RUN {0}"], }), 0x072 => Some(TokenDef { len: 2, rules: &["RUN {#id}"], }), 0x073 => Some(TokenDef { len: 0, rules: &["RUN"], }), 0x074 => Some(TokenDef { len: 2, rules: &["SELECT CASE {0}"], }), 0x075 => Some(TokenDef { len: 2, rules: &["STOP"], }), 0x076 => Some(TokenDef { len: -1, rules: &["subdef::={#procdecl}"], }), 0x077 => Some(TokenDef { len: 0, rules: &["WAIT {1}, {0}"], }), 0x078 => Some(TokenDef { len: 0, rules: &["WAIT {2}, {1}, {0}"], }), 0x079 => Some(TokenDef { len: 2, rules: &["WEND"], }), 0x07a => Some(TokenDef { len: 2, rules: &["WHILE {0}"], }), 0x07b => Some(TokenDef { len: 0, rules: &[""], }), 0x07c => Some(TokenDef { len: 0, rules: &[""], }), 0x07d => Some(TokenDef { len: 0, rules: &["prnmod::={prnmod:1} {0},", "prnmod::=PRINT {0},"], }), 0x07e => Some(TokenDef { len: 0, rules: &["{0}"], }), 0x07f => Some(TokenDef { len: 0, rules: &["{0}"], }), 0x080 => Some(TokenDef { len: 0, rules: &["{0}"], }), 0x081 => Some(TokenDef { len: 0, rules: &["1st-coord::=({1}, {0})"], }), 0x082 => Some(TokenDef { len: 0, rules: &["1st-coord::=STEP({1}, {0})"], }), 0x083 => Some(TokenDef { len: 0, rules: &["{1st-coord:2}-({1}, {0})", "({1}, {0})"], }), 0x084 => Some(TokenDef { len: 0, rules: &["{1st-coord:2}-STEP({1}, {0})", "-STEP({1}, {0})"], }), 0x085 => Some(TokenDef { len: 0, rules: &["FIELD {0}"], }), 0x086 => Some(TokenDef { len: 0, rules: &[", {1} AS {0}"], }), 0x087 => Some(TokenDef { len: 0, rules: &["finput::=INPUT {0},"], }), 0x088 => Some(TokenDef { len: 0, rules: &["{input|finput:1} {inputs:0}"], }), 0x089 => Some(TokenDef { len: -1, rules: &["input::=INPUT {##input-args}"], }), 0x08a => Some(TokenDef { len: 0, rules: &["#{0}"], }), 0x08c => Some(TokenDef { len: 2, rules: &[""], }), 0x08f => Some(TokenDef { len: 0, rules: &[ "prnsmc::={self|prncma|prnsrl:1} SPC({0});", "prnsmc::=SPC({0});", ], }), 0x090 => Some(TokenDef { len: 0, rules: &[ "prnsmc::={self|prncma|prnsrl:1} TAB({0});", "prnsmc::=TAB({0});", ], }), 0x091 => Some(TokenDef { len: 0, rules: &["prncma::={self|prnsmc|prnsrl:0} ,", "prncma::=,"], }), 0x092 => Some(TokenDef { len: 0, rules: &[ "prnsmc::={self:0}", "prnsmc::={prncma|prnsrl:0} ;", "prnsmc::=;", ], }), 0x093 => Some(TokenDef { len: 0, rules: &[ "{prnmod:2} {prnuse:1} {prnsrl|prnsmc|prncma:0}", "{prnmod:1} {prnsrl|prnsmc|prncma:0}", "{prnmod:1} {prnuse:0}", "{prnmod:1}", "PRINT {prnuse:1} {prnsrl|prnsmc|prncma:0}", "PRINT {prnsrl|prnsmc|prncma:0}", "PRINT {prnuse:0}", "PRINT", ], }), 0x094 => Some(TokenDef { len: 0, rules: &[ "prnsrl::={prncma|prnsmc|self:1} {expr:0},", "prnsrl::={expr:0},", ], }), 0x095 => Some(TokenDef { len: 0, rules: &[ "prnsrl::={prncma|prnsmc|self:1} {expr:0};", "prnsrl::={expr:0};", ], }), 0x096 => Some(TokenDef { len: 0, rules: &[ "{prnmod:3} {prnuse:2} {prnsmc|prncma|prnsrl:1} {expr:0}", "{prnmod:2} {prnsmc|prncma|prnsrl:1} {expr:0}", "{prnmod:1} {prnsmc|prncma|prnsrl|expr:0}", "PRINT {prnuse:2} {prnsmc|prncma|prnsrl:1} {expr:0}", "PRINT {prnsmc|prncma|prnsrl:1} {expr:0}", "PRINT {prnsmc|prncma|prnsrl|expr:0}", ], }), 0x097 => Some(TokenDef { len: -1, rules: &["{#tabi:0}'{#raw:2}"], }), 0x099 => Some(TokenDef { len: -1, rules: &["$INCLUDE: '{#raw:0}"], }), 0x09a => Some(TokenDef { len: 0, rules: &["BEEP"], }), 0x09b => Some(TokenDef { len: 0, rules: &["BLOAD {0}"], }), 0x09c => Some(TokenDef { len: 0, rules: &["BLOAD {1}, {0}"], }), 0x09d => Some(TokenDef { len: 0, rules: &["BSAVE {2}, {1}, {0}"], }), 0x09e => Some(TokenDef { len: 0, rules: &["CHDIR {0}"], }), 0x09f => Some(TokenDef { len: 0, rules: &["CIRCLE {##circle-args}"], }), 0x0a0 => Some(TokenDef { len: 0, rules: &["CIRCLE {##circle-args}"], }), 0x0a1 => Some(TokenDef { len: 2, rules: &["CLEAR{##varargs}"], }), 0x0a2 => Some(TokenDef { len: 2, rules: &["CLOSE{##varargs}"], }), 0x0a3 => Some(TokenDef { len: 0, rules: &["CLS {expr:0}", "CLS "], }), 0x0a4 => Some(TokenDef { len: 2, rules: &["COLOR{##varargs}"], }), 0x0a5 => Some(TokenDef { len: 4, rules: &[ "decl::=COMMON {declmod:0}{#blockname:2}", "decl::=COMMON{#blockname:2}", ], }), 0x0a6 => Some(TokenDef { len: -1, rules: &["DATA{#cstr:2}"], }), 0x0a7 => Some(TokenDef { len: 0, rules: &["DATE$ = {0}"], }), 0x0a8 => Some(TokenDef { len: 0, rules: &["DEF SEG"], }), 0x0a9 => Some(TokenDef { len: 0, rules: &["DEF SEG = {0}"], }), 0x0aa => Some(TokenDef { len: 0, rules: &["DRAW {0}"], }), 0x0ab => Some(TokenDef { len: 0, rules: &["ENVIRON {0}"], }), 0x0ac => Some(TokenDef { len: 2, rules: &["ERASE{##varargs}"], }), 0x0ad => Some(TokenDef { len: 0, rules: &["ERROR {0}"], }), 0x0ae => Some(TokenDef { len: 0, rules: &["FILES"], }), 0x0af => Some(TokenDef { len: 0, rules: &["FILES {0}"], }), 0x0b0 => Some(TokenDef { len: 0, rules: &["GET {0}"], }), 0x0b1 => Some(TokenDef { len: 0, rules: &["GET {1}, {0}"], }), 0x0b2 => Some(TokenDef { len: 2, rules: &["GET {1}, , {0}"], }), 0x0b3 => Some(TokenDef { len: 2, rules: &["GET {2}, {1}, {0}"], }), 0x0b4 => Some(TokenDef { len: 0, rules: &["GET {1}, {0}"], }), 0x0b5 => Some(TokenDef { len: 2, rules: &["PUT {1}, {0}, {#action-verb}"], }), 0x0b6 => Some(TokenDef { len: 0, rules: &["inputs::={inputs:1}, {0}", "inputs::={0}"], }), 0x0b7 => Some(TokenDef { len: 0, rules: &["IOCTL {1}, {0}"], }), 0x0b8 => Some(TokenDef { len: 2, rules: &["KEY {#keymode}"], }), 0x0b9 => Some(TokenDef { len: 0, rules: &["KEY {1}, {0}"], }), 0x0ba => Some(TokenDef { len: 0, rules: &["KILL {0}"], }), 0x0bb => Some(TokenDef { len: 2, rules: &["LINE {##line-args}"], }), 0x0bc => Some(TokenDef { len: 2, rules: &["LINE {##line-args}"], }), 0x0bd => Some(TokenDef { len: 2, rules: &["LINE {##line-args}"], }), 0x0be => Some(TokenDef { len: 2, rules: &["LINE {##line-args}"], }), 0x0bf => Some(TokenDef { len: 0, rules: &["LET "], }), 0x0c0 => Some(TokenDef { len: 2, rules: &[ "input::=LINE {finput:1} {0}", "input::=LINE INPUT {##input-args} {0}", ], }), 0x0c1 => Some(TokenDef { len: 2, rules: &["LOCATE{##varargs}"], }), 0x0c2 => Some(TokenDef { len: 2, rules: &["LOCK {##lock-args}"], }), 0x0c3 => Some(TokenDef { len: 0, rules: &["prnmod::=LPRINT"], }), 0x0c4 => Some(TokenDef { len: 0, rules: &["LSET {0} = {1}"], }), 0x0c5 => Some(TokenDef { len: 0, rules: &["MID$({0}, {2}) = {1}"], }), 0x0c6 => Some(TokenDef { len: 0, rules: &["MID$({0}, {3}, {2}) = {1}"], }), 0x0c7 => Some(TokenDef { len: 0, rules: &["MKDIR {0}"], }), 0x0c8 => Some(TokenDef { len: 0, rules: &["NAME {1} AS {0}"], }), 0x0c9 => Some(TokenDef { len: 2, rules: &["OPEN {1} {#open-args} AS {0}"], }), 0x0ca => Some(TokenDef { len: 2, rules: &["OPEN {2} {#open-args} AS {1} LEN = {0}"], }), 0x0cb => Some(TokenDef { len: 0, rules: &["OPEN {2}, {1}, {0}"], }), 0x0cc => Some(TokenDef { len: 0, rules: &["OPEN {3}, {2}, {1}, {0}"], }), 0x0cd => Some(TokenDef { len: 0, rules: &["OPTION BASE 0"], }), 0x0ce => Some(TokenDef { len: 0, rules: &["OPTION BASE 1"], }), 0x0cf => Some(TokenDef { len: 0, rules: &["OUT {1}, {0}"], }), 0x0d0 => Some(TokenDef { len: 0, rules: &[ "PAINT {2}{nularg:1}{nularg:0}", "PAINT {2}, {nularg:1}, {0}", "PAINT {2}, {1}{nularg:0}", "PAINT {2}, {1}, {0}", ], }), 0x0d1 => Some(TokenDef { len: 0, rules: &["PAINT {3}, {2}, {1}, {0}"], }), 0x0d2 => Some(TokenDef { len: 0, rules: &["PALETTE"], }), 0x0d3 => Some(TokenDef { len: 0, rules: &["PALETTE {1}, {0}"], }), 0x0d4 => Some(TokenDef { len: 0, rules: &["PALETTE {0}"], }), 0x0d5 => Some(TokenDef { len: 0, rules: &["PCOPY {1}, {0}"], }), 0x0d6 => Some(TokenDef { len: 0, rules: &["PLAY {0}"], }), 0x0d7 => Some(TokenDef { len: 0, rules: &["POKE {1}, {0}"], }), 0x0d8 => Some(TokenDef { len: 0, rules: &["PRESET {0}"], }), 0x0d9 => Some(TokenDef { len: 0, rules: &["PRESET {0}, {1}"], }), 0x0da => Some(TokenDef { len: 0, rules: &["PSET {0}"], }), 0x0db => Some(TokenDef { len: 0, rules: &["PSET {1}, {0}"], }), 0x0dd => Some(TokenDef { len: 0, rules: &["PUT {1}, {0}"], }), 0x0de => Some(TokenDef { len: 2, rules: &["PUT {1}, , {0}"], }), 0x0df => Some(TokenDef { len: 2, rules: &["PUT {2}, {1}, {0}"], }), 0x0e0 => Some(TokenDef { len: 0, rules: &["RANDOMIZE"], }), 0x0e1 => Some(TokenDef { len: 0, rules: &["RANDOMIZE {0}"], }), 0x0e2 => Some(TokenDef { len: 0, rules: &["{self:1}, {0}", "READ {0}"], }), 0x0e3 => Some(TokenDef { len: -1, rules: &["REM{#raw}"], }), 0x0e4 => Some(TokenDef { len: 0, rules: &["RESET"], }), 0x0e5 => Some(TokenDef { len: 0, rules: &["RMDIR {0}"], }), 0x0e6 => Some(TokenDef { len: 0, rules: &["RSET {0} = {1}"], }), 0x0e7 => Some(TokenDef { len: 2, rules: &["SCREEN{##varargs}"], }), 0x0e8 => Some(TokenDef { len: 0, rules: &["SEEK {1}, {0}"], }), 0x0e9 => Some(TokenDef { len: 0, rules: &["SHELL"], }), 0x0ea => Some(TokenDef { len: 0, rules: &["SHELL {0}"], }), 0x0eb => Some(TokenDef { len: 0, rules: &["SLEEP"], }), 0x0ec => Some(TokenDef { len: 0, rules: &["SOUND {1}, {0}"], }), 0x0ed => Some(TokenDef { len: 2, rules: &["SWAP {1}, {0}"], }), 0x0ee => Some(TokenDef { len: 0, rules: &["SYSTEM"], }), 0x0ef => Some(TokenDef { len: 0, rules: &["TIME$ = {0}"], }), 0x0f0 => Some(TokenDef { len: 0, rules: &["TROFF"], }), 0x0f1 => Some(TokenDef { len: 0, rules: &["TRON"], }), 0x0f2 => Some(TokenDef { len: 2, rules: &["UNLOCK {##lock-args}"], }), 0x0f3 => Some(TokenDef { len: 0, rules: &[ "VIEW ({5}, {4})-({3}, {2}){nularg:1}{nularg:0}", "VIEW ({5}, {4})-({3}, {2}), {nularg:1}, {0}", "VIEW ({5}, {4})-({3}, {2}), {1}{nularg:0}", "VIEW ({5}, {4})-({3}, {2})", ], }), 0x0f4 => Some(TokenDef { len: 0, rules: &["VIEW"], }), 0x0f5 => Some(TokenDef { len: 0, rules: &["VIEW PRINT"], }), 0x0f6 => Some(TokenDef { len: 0, rules: &["VIEW PRINT {1} TO {0}"], }), 0x0f7 => Some(TokenDef { len: 0, rules: &[ "VIEW SCREEN ({5}, {4})-({3}, {2}){nularg:1}{nularg:0}", "VIEW SCREEN ({5}, {4})-({3}, {2}), {nularg:1}, {0}", "VIEW SCREEN ({5}, {4})-({3}, {2}), {1}{nularg:0}", "VIEW SCREEN ({5}, {4})-({3}, {2})", ], }), 0x0f8 => Some(TokenDef { len: 0, rules: &["WIDTH {1}{nularg:0}", "WIDTH {1}, {0}"], }), 0x0f9 => Some(TokenDef { len: 0, rules: &["WIDTH LPRINT {0}"], }), 0x0fa => Some(TokenDef { len: 0, rules: &["WIDTH {1}, {0}"], }), 0x0fb => Some(TokenDef { len: 0, rules: &["WINDOW ({3}, {2})-({1}, {0})"], }), 0x0fc => Some(TokenDef { len: 0, rules: &["WINDOW"], }), 0x0fd => Some(TokenDef { len: 0, rules: &["WINDOW SCREEN ({3}, {2})-({1}, {0})"], }), 0x0fe => Some(TokenDef { len: 0, rules: &["prnmod::=WRITE"], }), 0x0ff => Some(TokenDef { len: 0, rules: &["prnuse::=USING {0};"], }), 0x100 => Some(TokenDef { len: 0, rules: &["expr::={1} + {0}"], }), 0x101 => Some(TokenDef { len: 0, rules: &["expr::={1} AND {0}"], }), 0x102 => Some(TokenDef { len: 0, rules: &["expr::={1} / {0}"], }), 0x103 => Some(TokenDef { len: 0, rules: &["expr::={1} = {0}"], }), 0x104 => Some(TokenDef { len: 0, rules: &["expr::={1} EQV {0}"], }), 0x105 => Some(TokenDef { len: 0, rules: &["expr::=ABS({0})"], }), 0x106 => Some(TokenDef { len: 0, rules: &["expr::=ASC({0})"], }), 0x107 => Some(TokenDef { len: 0, rules: &["expr::=ATN({0})"], }), 0x108 => Some(TokenDef { len: 0, rules: &["expr::=C{#type-abbr}({0})"], }), 0x109 => Some(TokenDef { len: 0, rules: &["expr::=CHR$({0})"], }), 0x10a => Some(TokenDef { len: 0, rules: &["expr::=COMMAND$"], }), 0x10b => Some(TokenDef { len: 0, rules: &["expr::=COS({0})"], }), 0x10c => Some(TokenDef { len: 0, rules: &["expr::=CSRLIN"], }), 0x10d => Some(TokenDef { len: 0, rules: &["expr::=CVD({0})"], }), 0x10e => Some(TokenDef { len: 0, rules: &["expr::=CVDMBD({0})"], }), 0x10f => Some(TokenDef { len: 0, rules: &["expr::=CVI({0})"], }), 0x110 => Some(TokenDef { len: 0, rules: &["expr::=CVL({0})"], }), 0x111 => Some(TokenDef { len: 0, rules: &["expr::=CVS({0})"], }), 0x112 => Some(TokenDef { len: 0, rules: &["expr::=CVSMBF({0})"], }), 0x113 => Some(TokenDef { len: 0, rules: &["expr::=DATE$"], }), 0x114 => Some(TokenDef { len: 0, rules: &["expr::=ENVIRON$({0})"], }), 0x115 => Some(TokenDef { len: 0, rules: &["expr::=EOF({0})"], }), 0x116 => Some(TokenDef { len: 0, rules: &["expr::=ERDEV"], }), 0x117 => Some(TokenDef { len: 0, rules: &["expr::=ERDEV$"], }), 0x118 => Some(TokenDef { len: 0, rules: &["expr::=ERL"], }), 0x119 => Some(TokenDef { len: 0, rules: &["expr::=ERR"], }), 0x11a => Some(TokenDef { len: 0, rules: &["expr::=EXP({0})"], }), 0x11b => Some(TokenDef { len: 0, rules: &["expr::=FILEATTR({1}, {0})"], }), 0x11c => Some(TokenDef { len: 0, rules: &["expr::=FIX({0})"], }), 0x11d => Some(TokenDef { len: 0, rules: &["expr::=FRE({0})"], }), 0x11e => Some(TokenDef { len: 0, rules: &["expr::=FREEFILE"], }), 0x11f => Some(TokenDef { len: 0, rules: &["expr::=HEX$({0})"], }), 0x120 => Some(TokenDef { len: 0, rules: &["expr::=INKEY$"], }), 0x121 => Some(TokenDef { len: 0, rules: &["expr::=INP({0})"], }), 0x122 => Some(TokenDef { len: 0, rules: &["expr::=INPUT$({0})"], }), 0x123 => Some(TokenDef { len: 0, rules: &["expr::=INPUT$({1}, {0})"], }), 0x124 => Some(TokenDef { len: 0, rules: &["expr::=INSTR({1}, {0})"], }), 0x125 => Some(TokenDef { len: 0, rules: &["expr::=INSTR({2}, {1}, {0})"], }), 0x126 => Some(TokenDef { len: 0, rules: &["expr::=INT({0})"], }), 0x127 => Some(TokenDef { len: 0, rules: &["expr::=IOCTL$({0})"], }), 0x128 => Some(TokenDef { len: 0, rules: &["expr::=LBOUND({0})"], }), 0x129 => Some(TokenDef { len: 0, rules: &["expr::=LBOUND({1}, {0})"], }), 0x12a => Some(TokenDef { len: 0, rules: &["expr::=LCASE$({0})"], }), 0x12b => Some(TokenDef { len: 0, rules: &["expr::=LTRIM$({0})"], }), 0x12c => Some(TokenDef { len: 0, rules: &["expr::=LEFT$({1}, {0})"], }), 0x12d => Some(TokenDef { len: 2, rules: &["expr::=LEN({0})"], }), 0x12e => Some(TokenDef { len: 0, rules: &["expr::=LOC({0})"], }), 0x12f => Some(TokenDef { len: 0, rules: &["expr::=LOF({0})"], }), 0x130 => Some(TokenDef { len: 0, rules: &["expr::=LOG({0})"], }), 0x131 => Some(TokenDef { len: 0, rules: &["expr::=LPOS({0})"], }), 0x132 => Some(TokenDef { len: 0, rules: &["expr::=MID$({1}, {0})"], }), 0x133 => Some(TokenDef { len: 0, rules: &["expr::=MID$({2}, {1}, {0})"], }), 0x134 => Some(TokenDef { len: 0, rules: &["expr::=MKD$({0})"], }), 0x135 => Some(TokenDef { len: 0, rules: &["expr::=MKDMBF$({0})"], }), 0x136 => Some(TokenDef { len: 0, rules: &["expr::=MKI$({0})"], }), 0x137 => Some(TokenDef { len: 0, rules: &["expr::=MKL$({0})"], }), 0x138 => Some(TokenDef { len: 0, rules: &["expr::=MKS$({expr:0})", "{0}"], }), 0x139 => Some(TokenDef { len: 0, rules: &["expr::=MKSMBF({0})"], }), 0x13a => Some(TokenDef { len: 0, rules: &["expr::=OCT$({0})"], }), 0x13b => Some(TokenDef { len: 0, rules: &["expr::=PEEK({0})"], }), 0x13c => Some(TokenDef { len: 0, rules: &["expr::=PEN"], }), 0x13d => Some(TokenDef { len: 0, rules: &["expr::=PLAY"], }), 0x13e => Some(TokenDef { len: 0, rules: &["expr::=PMAP({1}, {0})"], }), 0x13f => Some(TokenDef { len: 0, rules: &["expr::=POINT({0})"], }), 0x140 => Some(TokenDef { len: 0, rules: &["expr::=POINT({1}, {0})"], }), 0x141 => Some(TokenDef { len: 0, rules: &["expr::=POS({0})"], }), 0x142 => Some(TokenDef { len: 0, rules: &["expr::=RIGHT$({1}, {0})"], }), 0x143 => Some(TokenDef { len: 0, rules: &["expr::=RND"], }), 0x144 => Some(TokenDef { len: 0, rules: &["expr::=RND({0})"], }), 0x145 => Some(TokenDef { len: 0, rules: &["expr::=RTRIM$({0})"], }), 0x146 => Some(TokenDef { len: 0, rules: &["expr::=SADD({0})"], }), 0x147 => Some(TokenDef { len: 0, rules: &["expr::=SCREEN({1}, {0})"], }), 0x148 => Some(TokenDef { len: 0, rules: &["expr::=SCREEN({2}, {1}, {0})"], }), 0x149 => Some(TokenDef { len: 0, rules: &["expr::=SEEK({0})"], }), 0x14a => Some(TokenDef { len: 0, rules: &["expr::=SETMEM({0})"], }), 0x14b => Some(TokenDef { len: 0, rules: &["expr::=SGN({0})"], }), 0x14c => Some(TokenDef { len: 0, rules: &["expr::=SHELL({0})"], }), 0x14d => Some(TokenDef { len: 0, rules: &["expr::=SIN({0})"], }), 0x14e => Some(TokenDef { len: 0, rules: &["expr::=SPACE$({0})"], }), 0x14f => Some(TokenDef { len: 0, rules: &["expr::=SQR({0})"], }), 0x150 => Some(TokenDef { len: 0, rules: &["expr::=STICK({0})"], }), 0x151 => Some(TokenDef { len: 0, rules: &["expr::=STR$({0})"], }), 0x152 => Some(TokenDef { len: 0, rules: &["expr::=STRIG({0})"], }), 0x153 => Some(TokenDef { len: 0, rules: &["expr::=STRING$({1}, {0})"], }), 0x154 => Some(TokenDef { len: 0, rules: &["expr::=TAN({0})"], }), 0x155 => Some(TokenDef { len: 0, rules: &["expr::=TIME$"], }), 0x156 => Some(TokenDef { len: 0, rules: &["expr::=TIMER"], }), 0x157 => Some(TokenDef { len: 0, rules: &["expr::=UBOUND({0})"], }), 0x158 => Some(TokenDef { len: 0, rules: &["expr::=UBOUND({1}, {0})"], }), 0x159 => Some(TokenDef { len: 0, rules: &["expr::=UCASE$({0})"], }), 0x15a => Some(TokenDef { len: 0, rules: &["expr::=VAL({0})"], }), 0x15b => Some(TokenDef { len: 0, rules: &["expr::=VARPTR({0})"], }), 0x15c => Some(TokenDef { len: 2, rules: &["expr::=VARPTR$({0})"], }), 0x15d => Some(TokenDef { len: 0, rules: &["expr::=VARSEG({0})"], }), 0x15e => Some(TokenDef { len: 0, rules: &["expr::={1} >= {0}"], }), 0x15f => Some(TokenDef { len: 0, rules: &["expr::={1} > {0}"], }), 0x160 => Some(TokenDef { len: 0, rules: &["expr::={1} \\ {0}"], }), 0x161 => Some(TokenDef { len: 0, rules: &["expr::={1} IMP {0}"], }), 0x162 => Some(TokenDef { len: 0, rules: &["expr::={1} <= {0}"], }), 0x163 => Some(TokenDef { len: 0, rules: &["expr::={1} < {0}"], }), 0x164 => Some(TokenDef { len: 0, rules: &["expr::={#hprm}"], }), 0x165 => Some(TokenDef { len: 2, rules: &["expr::={#int}"], }), 0x166 => Some(TokenDef { len: 4, rules: &["expr::={#lng}"], }), 0x167 => Some(TokenDef { len: 2, rules: &["expr::={#int&h}"], }), 0x168 => Some(TokenDef { len: 4, rules: &["expr::={#lng&h}"], }), 0x169 => Some(TokenDef { len: 2, rules: &["expr::={#int&o}"], }), 0x16a => Some(TokenDef { len: 4, rules: &["expr::={#lng&o}"], }), 0x16b => Some(TokenDef { len: 4, rules: &["expr::={#sng}"], }), 0x16c => Some(TokenDef { len: 8, rules: &["expr::={#dbl}"], }), 0x16d => Some(TokenDef { len: -1, rules: &["expr::={#qstr}"], }), 0x16e => Some(TokenDef { len: 0, rules: &["expr::=({0})"], }), 0x16f => Some(TokenDef { len: 0, rules: &["expr::={1} MOD {0}"], }), 0x170 => Some(TokenDef { len: 0, rules: &["expr::={1} * {0}"], }), 0x171 => Some(TokenDef { len: 0, rules: &["expr::={1} <> {0}"], }), 0x172 => Some(TokenDef { len: 0, rules: &["expr::={#nul}"], }), 0x173 => Some(TokenDef { len: 0, rules: &["nularg::={#nul}"], }), 0x174 => Some(TokenDef { len: 0, rules: &["expr::=NOT {0}"], }), 0x175 => Some(TokenDef { len: 0, rules: &["expr::={1} OR {0}"], }), 0x176 => Some(TokenDef { len: 0, rules: &["expr::={1} ^ {0}"], }), 0x177 => Some(TokenDef { len: 0, rules: &["expr::={1} - {0}"], }), 0x178 => Some(TokenDef { len: 0, rules: &["expr::=-{0}"], }), 0x179 => Some(TokenDef { len: 0, rules: &["expr::={1} XOR {0}"], }), 0x17a => Some(TokenDef { len: 0, rules: &["UEVENT"], }), 0x17b => Some(TokenDef { len: 0, rules: &["SLEEP {0}"], }), 0x17c => Some(TokenDef { len: 6, rules: &["astype::={#tabi:4}AS STRING * {#int:2}"], }), 0x17d => Some(TokenDef { len: 2, rules: &["decl::=DIM {declmod:0}", "decl::=DIM"], }), 0x181 => Some(TokenDef { len: 0, rules: &["expr::=CURDIR$"], }), 0x183 => Some(TokenDef { len: 0, rules: &["expr::=ERROR$"], }), 0x184 => Some(TokenDef { len: 0, rules: &["expr::=DIR$({0})"], }), 0x186 => Some(TokenDef { len: 0, rules: &["expr::=BOF({0})"], }), 0x188 => Some(TokenDef { len: 0, rules: &["expr::=GETINDEX$({0})"], }), 0x194 => Some(TokenDef { len: 0, rules: &["expr::={0}"], }), 0x192 => Some(TokenDef { len: 0, rules: &["DELETE {0}"], }), 0x197 => Some(TokenDef { len: 0, rules: &["INSERT {1}, {0}"], }), 0x198 => Some(TokenDef { len: 2, rules: &[ "{#method:0}MOVEFIRST {0}", "{#method:4}MOVELAST {0}", "{#method:8}MOVENEXT {0}", "{#method:12}MOVEPREVIOUS {0}", ], }), 0x199 => Some(TokenDef { len: 2, rules: &["ON LOCAL ERROR GOTO {#id}"], }), 0x19b => Some(TokenDef { len: 2, rules: &["RETRIEVE {1}, {0}"], }), 0x19f => Some(TokenDef { len: 0, rules: &["SEEKGT {1}, {0}"], }), 0x1a1 => Some(TokenDef { len: 0, rules: &["SETINDEX {1}, {0}"], }), 0x1a8 => Some(TokenDef { len: 0, rules: &["REDIM {0}"], }), 0x1a9 => Some(TokenDef { len: 0, rules: &["implicit-control::="], }), 0x1aa => Some(TokenDef { len: 2, rules: &["expr::=TYPEOF {0} IS {#id}"], }), 0x1ab => Some(TokenDef { len: 0, rules: &["LOAD {0}"], }), 0x1ac => Some(TokenDef { len: 0, rules: &["UNLOAD {0}"], }), 0x1ad => Some(TokenDef { len: 0, rules: &["expr::=DOEVENTS"], }), 0x1b0 => Some(TokenDef { len: 0, rules: &["expr::=ERROR$"], }), 0x1b2 => Some(TokenDef { len: 0, rules: &["expr::=STR$({expr:0})"], }), 0x1b3 => Some(TokenDef { len: 0, rules: &["expr::=FORMAT$({1}, {0})"], }), 0x1ba => Some(TokenDef { len: 0, rules: &["expr::=DATE$"], }), 0x1bc => Some(TokenDef { len: 0, rules: &["expr::=TIMEVALUE({0})"], }), 0x1c0 => Some(TokenDef { len: 2, rules: &[""], }), 0x1c1 => Some(TokenDef { len: 0, rules: &["expr::={0}"], }), 0x1c2 => Some(TokenDef { len: -1, rules: &[""], }), 0x1c3 => Some(TokenDef { len: 0, rules: &["expr::=INPUTBOX$({2}, {1}, {0})"], }), 0x1c5 => Some(TokenDef { len: 0, rules: &[""], }), 0x1c6 => Some(TokenDef { len: 0, rules: &[""], }), 0x1c7 | 0x1c8 => Some(TokenDef { len: 0, rules: &["nularg::="], }), 0x1c9 => Some(TokenDef { len: 2, rules: &[ "controlstmt::={#method:1}{0}.CLS", "controlstmt::={#method:2}{implicit-control:0}HIDE", "controlstmt::={#method:2}{0}.HIDE", "prnmod::={#method:4}{0}.PRINT", "controlstmt::={#method:6}{0}.REFRESH", "controlstmt::={#method:8}{0}.SETFOCUS", "controlstmt::={#method:9}{implicit-control:0}SHOW", "controlstmt::={#method:9}{0}.SHOW", "controlstmt::={#method:14}{0}.ENDDOC", "controlstmt::={#method:16}{0}.NEWPAGE", ], }), 0x1ca => Some(TokenDef { len: 2, rules: &["{1}.ADDITEM {0}"], }), 0x1cb => Some(TokenDef { len: 2, rules: &["{1}.REMOVEITEM {0}"], }), 0x1cc => Some(TokenDef { len: 2, rules: &["{2}.MOVE {1}, {0}"], }), 0x1ce => Some(TokenDef { len: 4, rules: &["{4}.MOVE {3}, {2}, {1}, {0}"], }), 0x1d0 => Some(TokenDef { len: 2, rules: &["expr::={0}.GETTEXT"], }), 0x3ff => Some(TokenDef { len: 0, rules: &[""], }), _ => None, } } #[derive(Clone)] struct Item { kind: String, text: String, } struct Decoder<'a> { file: &'a str, sym: &'a [u8], code: &'a [u8], base: usize, at: usize, pcode: u16, hparam: u16, stack: Vec, pending: String, lines: Vec, procedure_type: &'static str, modified_defaults: u32, return_name: Option, return_suffix: &'static str, } impl<'a> Decoder<'a> { fn new(file: &'a str, sym: &'a [u8], code: &'a [u8], base: usize) -> Self { Self { file, sym, code, base, at: 0, pcode: 0, hparam: 0, stack: Vec::new(), pending: String::new(), lines: Vec::new(), procedure_type: "SUB", modified_defaults: 0, return_name: None, return_suffix: "", } } fn fail(&self, at: usize, name: impl Into, message: impl Into) -> FormError { error(self.file, self.base + at, name, message) } fn ident(&self, offset: u16) -> Result { if matches!(offset, u16::MAX | 0xfffe) { return Ok("0".into()); } let at = offset as usize; let header = self.sym.get(at..at + 4).ok_or_else(|| { self.fail( self.at, format!("Symbol 0x{offset:04x}"), "ungueltiger Symbolverweis", ) })?; if header[2] & 2 != 0 { return read_u16(self.sym, at + 4) .map(|value| value.to_string()) .ok_or_else(|| self.fail(self.at, "Symboltabelle", "abgeschnittene Zeilennummer")); } let len = header[3] as usize; self.sym .get(at + 4..at + 4 + len) .map(decode_cp437) .ok_or_else(|| self.fail(self.at, "Symboltabelle", "abgeschnittener Name")) } fn suffix(kind: u16) -> &'static str { match kind { 1 => "%", 2 => "&", 3 => "!", 4 => "#", 5 => "@", 7 => "$", _ => "", } } fn type_name(&self, kind: u16) -> Result { Ok(match kind { 0 => "ANY".into(), 1 => "INTEGER".into(), 2 => "LONG".into(), 3 => "SINGLE".into(), 4 => "DOUBLE".into(), 5 => "CURRENCY".into(), 7 => "STRING".into(), 9 => "CONTROL".into(), // VBDOS verwendet fuer eingebaute Typen zusaetzlich die // internen Kennungen des Editors. 0x013e => "STRING".into(), other => self.ident(other)?, }) } fn peek(&self, offset: usize) -> Option<&Item> { self.stack.get(self.stack.len().checked_sub(offset + 1)?) } fn split_tag<'b>(&self, tag: &'b str) -> (&'b str, isize, bool) { if let Some(value) = tag.strip_prefix('$') { return ("", value.parse().unwrap_or(0), true); } if let Some((kind, value)) = tag.rsplit_once(':') { let relative = value.starts_with('$'); let value = value.strip_prefix('$').unwrap_or(value); return (kind, value.parse().unwrap_or(0), relative); } if let Ok(offset) = tag.parse() { ("", offset, false) } else { (tag, 0, false) } } fn valid(&self, kind: &str, offset: isize, own: &str) -> bool { let Some(item) = usize::try_from(offset) .ok() .and_then(|offset| self.peek(offset)) else { return kind.is_empty(); }; kind.is_empty() || kind.split('|').any(|expected| { expected == "*" || item.kind == if expected == "self" { own } else { expected } }) } fn word(data: &[u8], at: usize) -> Option { read_u16(data, at) } fn call(&self, data: &[u8], parentheses: bool) -> Result { let count = Self::word(data, 0) .ok_or_else(|| self.fail(self.at, "CALL", "Argumentzahl fehlt"))? as usize; let args = (0..count) .rev() .map(|index| format!("{{{index}}}")) .collect::>() .join(", "); Ok(if args.is_empty() { String::new() } else if parentheses { format!("({args})") } else { format!(" {args}") }) } fn proc_decl( &mut self, data: &[u8], offset: usize, parentheses: bool, ) -> Result { let id = Self::word(data, offset).ok_or_else(|| self.fail(self.at, "Prozedur", "Name fehlt"))?; let flags = Self::word(data, offset + 2) .ok_or_else(|| self.fail(self.at, "Prozedur", "Flags fehlen"))?; let count = Self::word(data, offset + 4) .ok_or_else(|| self.fail(self.at, "Prozedur", "Argumentzahl fehlt"))? as usize; let mut name = self.ident(id)?; let kind = match (flags & 0x300) >> 8 { 1 => "SUB", 2 => "FUNCTION", 3 => "DEF", _ => "", }; if !kind.is_empty() { self.procedure_type = kind; name = format!("{kind} {name}"); } if flags & 0x80 != 0 { let suffix = Self::suffix(flags & 7); if self.pcode == 0x058 { self.return_name = Some(name.strip_prefix("FUNCTION ").unwrap_or(&name).into()); self.return_suffix = suffix; } name.push_str(suffix); } let mut args = Vec::with_capacity(count); for index in 1..=count { let at = offset + 6 + (index - 1) * 8; let mut arg = self.ident( Self::word(data, at) .ok_or_else(|| self.fail(self.at, "Prozedurargument", "Name fehlt"))?, )?; let arg_flags = Self::word(data, at + 2) .ok_or_else(|| self.fail(self.at, "Prozedurargument", "Flags fehlen"))?; let arg_type = Self::word(data, at + 4) .ok_or_else(|| self.fail(self.at, "Prozedurargument", "Typ fehlt"))?; if arg_flags & 0x200 != 0 { arg.push_str(Self::suffix(arg_type)); } if arg_flags & 0x400 != 0 { arg.push_str("()"); } if arg_flags & 0x800 != 0 { arg = format!("SEG {arg}"); } if arg_flags & 0x1000 != 0 { arg = format!("BYVAL {arg}"); } if arg_flags & 0x2000 != 0 { let ty = self.type_name(arg_type)?; arg.push_str(&format!(" AS {ty}")); } args.push(arg); } if flags & 0x8000 != 0 { name.push_str(" CDECL"); } let alias_len = ((flags >> 10) & 0x1f) as usize; let tail = offset + 6 + count * 8; if alias_len != 0 { name.push_str(" ALIAS "); name.push_str(&decode_cp437( data.get(tail..tail + alias_len) .ok_or_else(|| self.fail(self.at, "ALIAS", "Name abgeschnitten"))?, )); } if parentheses || !args.is_empty() { name.push_str(" ("); name.push_str(&args.join(", ")); name.push(')'); } Ok(name) } fn array(&self, data: &[u8], declaration: bool) -> Result { let declaration = declaration || read_u16(self.code, self.at).is_some_and(|word| word & 0x3ff == 0x1a8); let count = Self::word(data, 0) .ok_or_else(|| self.fail(self.at, "Feld", "Dimension fehlt"))? as usize; let mut name = self .ident(Self::word(data, 2).ok_or_else(|| self.fail(self.at, "Feld", "Name fehlt"))?)?; name.push_str(Self::suffix(self.hparam)); let mut stack_offset = usize::from(declaration && self.peek(0).is_some_and(|item| item.kind == "astype")); if count != 0 { let mut dimensions = String::new(); if declaration { let mut remaining = count; while remaining != 0 { remaining -= 1; dimensions.insert_str(0, &format!("{{{stack_offset}}}")); stack_offset += 1; if remaining & 1 != 0 { if self.peek(stack_offset).is_none_or(|item| item.kind != "24") { dimensions.insert_str(0, " TO "); } } else if remaining != 0 { dimensions.insert_str(0, ", "); } } } else { dimensions = (0..count) .rev() .map(|offset| format!("{{{offset}}}")) .collect::>() .join(", "); stack_offset = count; } name.push('('); name.push_str(&dimensions); name.push(')'); } else if !declaration { name.push_str("()"); } if declaration && self.peek(0).is_some_and(|item| item.kind == "astype") { name.push_str("{0}"); } if declaration { if self .peek(stack_offset) .is_some_and(|item| item.kind == "decls") { return Ok(format!("{{{stack_offset}}}, {name}")); } if self .peek(stack_offset) .is_some_and(|item| item.kind == "decl") { return Ok(format!("{{{stack_offset}}} {name}")); } } Ok(name) } fn varargs(&self, data: &[u8]) -> Result { let count = Self::word(data, 0) .ok_or_else(|| self.fail(self.at, "Argumentliste", "Anzahl fehlt"))? as usize; let args = (0..count) .rev() .map(|index| format!("{{{index}}}")) .collect::>() .join(", "); Ok(if args.is_empty() { args } else { format!(" {args}") }) } fn data_tag(&mut self, tag: &str, offset: usize, data: &[u8]) -> Result { let word = || { Self::word(data, offset) .ok_or_else(|| self.fail(self.at, tag, "Tokendaten abgeschnitten")) }; Ok(match tag.to_ascii_lowercase().as_str() { "" | "nul" | "thaddr" | "optargs" => String::new(), "newline" => "\r\u{1}".into(), "newline-include" => "\r\u{2}".into(), "tabh" => format!( "\r\0{}{}", char::from(self.hparam as u8), char::from((self.hparam >> 8) as u8) ), "tabi" => { let value = word()?; format!( "\r\0{}{}", char::from(value as u8), char::from((value >> 8) as u8) ) } "indent" => " ".repeat(word()? as usize), "raw" => decode_pcode_bytes(data.get(offset..).unwrap_or_default()), "cstr" => decode_pcode_bytes( data.get(offset..) .unwrap_or_default() .split(|byte| *byte == 0) .next() .unwrap_or_default(), ), "qstr" => format!( "\"{}\"", decode_pcode_bytes(data.get(offset..).unwrap_or_default()) ), "id" => self.ident(word()?)?, "id+" => { let name = self.ident(word()?)?; let suffix = Self::suffix(self.hparam); format!( "{name}{}", if suffix.is_empty() && self.return_name.as_deref() == Some(&name) { self.return_suffix } else { suffix } ) } "id-list" => data .get(offset..) .unwrap_or_default() .chunks_exact(2) .map(|bytes| self.ident(u16::from_le_bytes([bytes[0], bytes[1]]))) .collect::, _>>()? .join(", "), "type" => self.type_name(word()?)?, "type-abbr" => ["", "INT", "LNG", "SNG", "DBL", "STR"] .get(self.hparam as usize) .copied() .unwrap_or("") .into(), "hprm" => self.hparam.to_string(), "int" => (word()? as i16).to_string(), "int&h" => format!("&H{:X}", word()?), "int&o" => format!("&O{:o}", word()?), "lng" => i32::from_le_bytes( data.get(offset..offset + 4) .ok_or_else(|| self.fail(self.at, tag, "LONG abgeschnitten"))? .try_into() .unwrap(), ) .to_string(), "lng&h" => format!( "&H{:X}", u32::from_le_bytes( data.get(offset..offset + 4) .ok_or_else(|| self.fail(self.at, tag, "LONG abgeschnitten"))? .try_into() .unwrap() ) ), "sng" => f32::from_le_bytes( data.get(offset..offset + 4) .ok_or_else(|| self.fail(self.at, tag, "SINGLE abgeschnitten"))? .try_into() .unwrap(), ) .to_string(), "dbl" => f64::from_le_bytes( data.get(offset..offset + 8) .ok_or_else(|| self.fail(self.at, tag, "DOUBLE abgeschnitten"))? .try_into() .unwrap(), ) .to_string(), "label" => { let name = self.ident(word()?)?; if name.bytes().all(|byte| byte.is_ascii_digit()) { format!("{name} ") } else { format!("{name}:") } } "call" => self.call(data, false)?, "call()" => self.call(data, true)?, "procdecl" => self.proc_decl(data, offset, false)?, "procdecl()" => self.proc_decl(data, offset, true)?, "id(expr)" => self.array(data, false)?, "id(decl)" => self.array(data, true)?, "varargs" => self.varargs(data)?, "proctype" => self.procedure_type.into(), "blockname" => { let id = word()?; if id == u16::MAX { String::new() } else { format!(" /{}/", self.ident(id)?) } } "action-verb" => { ["OR", "AND", "PRESET", "PSET", "XOR"][usize::from(word()?.min(4))].into() } "open-args" => { let flags = word()?; [ match flags & 0x3f { 1 => "FOR INPUT", 2 => "FOR OUTPUT", 4 => "FOR RANDOM", 8 => "FOR APPEND", 32 => "FOR BINARY", _ => "", }, match (flags >> 8) & 3 { 1 => "ACCESS READ", 2 => "ACCESS WRITE", 3 => "ACCESS READ WRITE", _ => "", }, match (flags >> 12) & 7 { 1 => "LOCK READ WRITE", 2 => "LOCK WRITE", 3 => "LOCK READ", 4 => "SHARED", _ => "", }, ] .into_iter() .filter(|part| !part.is_empty()) .collect::>() .join(" ") } "keymode" => match word()? { 1 => "ON", 2 => "LIST", _ => "OFF", } .into(), // Die im Corpus verwendeten Varianten haben genau diese Form; // die Stack-Platzhalter werden anschliessend normal aufgeloest. "line-args" | "lock-args" => "{0}".into(), "circle-args" => "{1}, {0}".into(), "input-args" => String::new(), "defxxx" => { let raw = data .get(offset + 2..offset + 6) .ok_or_else(|| self.fail(self.at, "DEF", "Tokendaten abgeschnitten"))?; let mask = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]); let kind = match mask & 7 { 1 => "INT", 2 => "LNG", 3 => "SNG", 4 => "DBL", 5 => "STR", _ => "", }; let letters = mask & 0xffff_ffc0; if mask & 7 == 3 { self.modified_defaults &= !letters; } else { self.modified_defaults |= letters; } format!("DEF{kind} {}", letter_ranges(letters)) } other => { return Err(self.fail( self.at, format!("Token-Tag {other}"), "unbekannte P-Code-Struktur", )) } }) } fn execute(&mut self, mut rule: String, data: &[u8]) -> Result { let mut kind = self.pcode.to_string(); if let Some((left, right)) = rule.split_once("::=") { kind = left.into(); rule = right.into(); } if rule.starts_with('.') { self.pending .extend(self.stack.drain(..).map(|item| item.text)); rule.remove(0); } let initial = self.stack.len(); let mut deepest = None::; let mut relative = Vec::<(String, String, isize)>::new(); let mut out = String::new(); let mut cursor = 0; while let Some(open) = rule[cursor..].find('{').map(|value| cursor + value) { let Some(close) = rule[open + 1..].find('}').map(|value| open + 1 + value) else { return Err(self.fail(self.at, "Tokenregel", "offene Ersetzung")); }; out.push_str(&rule[cursor..open]); let tag = &rule[open + 1..close]; if let Some(tag) = tag.strip_prefix("##") { let (tag, offset, _) = self.split_tag(tag); let replacement = self.data_tag(tag, offset.max(0) as usize, data)?; rule.replace_range(open..=close, &replacement); cursor = open; continue; } if let Some(tag) = tag.strip_prefix('#') { let (tag, offset, _) = self.split_tag(tag); if tag == "method" { if Self::word(data, 0) != Some(offset as u16) { return Ok(false); } cursor = close + 1; continue; } out.push_str(&self.data_tag(tag, offset.max(0) as usize, data)?); } else { let (expected, offset, is_relative) = self.split_tag(tag); if is_relative { let marker = format!("\u{b}{}\u{c}", relative.len()); out.push_str(&marker); relative.push((marker, expected.into(), offset)); } else { if !self.valid(expected, offset, &kind) { return Ok(false); } if let Some(item) = usize::try_from(offset) .ok() .and_then(|offset| self.peek(offset)) { out.push_str(&item.text); } deepest = deepest.max(usize::try_from(offset).ok()); } } cursor = close + 1; } out.push_str(&rule[cursor..]); let mut consume = deepest.map_or(0, |offset| offset + 1); for (marker, expected, offset) in relative { let actual = consume as isize + offset; if !self.valid(&expected, actual, &kind) { return Ok(false); } let text = self .peek(actual as usize) .map(|item| item.text.as_str()) .unwrap_or(""); out = out.replace(&marker, text); consume = consume.max(actual as usize + 1); } self.stack.truncate(initial.saturating_sub(consume)); self.stack.push(Item { kind, text: out }); Ok(true) } fn postprocess(&mut self, final_block: bool) { while let Some(marker) = self.pending.find('\r') { let kind = self.pending.as_bytes().get(marker + 1).copied(); if kind == Some(0) && marker + 3 < self.pending.len() { let bytes = self.pending.as_bytes(); let column = u16::from_le_bytes([bytes[marker + 2], bytes[marker + 3]]) as usize; let spaces = column.saturating_sub(marker).max(1); self.pending .replace_range(marker..marker + 4, &" ".repeat(spaces)); continue; } if matches!(kind, Some(1 | 2)) { let line = self.pending[..marker].trim_end().to_owned(); self.lines.push(line); self.pending.drain(..marker + 2); continue; } if let (Some(count), Some(ch)) = ( self.pending.as_bytes().get(marker + 1).copied(), self.pending.as_bytes().get(marker + 2).copied(), ) { self.pending.replace_range( marker..marker + 3, &char::from(ch).to_string().repeat(count as usize), ); continue; } break; } if final_block && !self.pending.trim().is_empty() { self.lines.push(self.pending.trim_end().into()); self.pending.clear(); } } fn run(mut self) -> Result<(String, u32), FormError> { while self.at < self.code.len() { let token_at = self.at; let word = read_u16(self.code, self.at) .ok_or_else(|| self.fail(self.at, "P-Code", "Token abgeschnitten"))?; self.at += 2; self.pcode = word & 0x3ff; self.hparam = word >> 10; if self.pcode == 8 { break; } let def = token_def(self.pcode).ok_or_else(|| { self.fail( token_at, format!("Token 0x{:03x}", self.pcode), "unbekannte P-Code-Struktur", ) })?; let len = if def.len < 0 { let len = read_u16(self.code, self.at) .ok_or_else(|| self.fail(self.at, "P-Code", "variable Laenge fehlt"))? as usize; self.at += 2; len } else { def.len as usize }; let data = self .code .get(self.at..self.at + len) .ok_or_else(|| self.fail(self.at, "P-Code", "Tokendaten abgeschnitten"))? .to_owned(); self.at += (len + 1) & !1; if matches!(self.pcode, 0x065 | 0x066) { let operands = usize::from(self.pcode == 0x066); let split = self.stack.len().saturating_sub(operands); if split > 0 && self.stack[split - 1].kind != "newline" && !self.stack[split - 1].text.ends_with(" THEN ") && !self.stack[split - 1].text.ends_with(" ELSE ") { self.stack.insert( split, Item { kind: "newline".into(), text: "\r\u{1}".into(), }, ); } } if matches!(self.pcode, 0x1c5 | 0x1c6) { let first = self .stack .len() .checked_sub(3) .ok_or_else(|| self.fail(token_at, "MSGBOX", "Argumente fehlen"))?; if self.stack[first..] .iter() .any(|item| !matches!(item.kind.as_str(), "expr" | "nularg")) { return Err(self.fail(token_at, "MSGBOX", "ungueltige Argumentstruktur")); } let mut args = self.stack.drain(first..).collect::>(); while args.last().is_some_and(|item| item.kind == "nularg") { args.pop(); } let call = format!( "MSGBOX{}{}", if self.pcode == 0x1c6 { "(" } else { " " }, args.iter() .map(|item| item.text.as_str()) .collect::>() .join(", ") ) + if self.pcode == 0x1c6 { ")" } else { "" }; self.stack.push(Item { kind: if self.pcode == 0x1c6 { "expr" } else { "statement" } .into(), text: call, }); self.postprocess(false); continue; } let mut accepted = false; for rule in def.rules { if self.execute((*rule).into(), &data)? { accepted = true; break; } } if !accepted { let stack = self .stack .iter() .map(|item| item.kind.as_str()) .collect::>() .join(", "); return Err(self.fail( token_at, format!("Token 0x{:03x}", self.pcode), format!("keine passende P-Code-Regel; Stapel: {stack}"), )); } self.postprocess(false); } self.pending .extend(self.stack.drain(..).map(|item| item.text)); self.postprocess(true); let output = if self.lines.is_empty() { String::new() } else { self.lines.join("\n") + "\n" }; Ok((output, self.modified_defaults)) } } type ModuleLayout = (usize, usize, Vec<(usize, usize)>); fn candidate_layout(bytes: &[u8], base: usize) -> Option { let symbol_len = usize::from(read_u16(bytes, base)?); let symbol_start = base + 2; let main_header = symbol_start.checked_add(symbol_len)?; let main_len = usize::from(read_u16(bytes, main_header)?); if main_len & 1 != 0 { return None; } let main_start = main_header + 2; let main_end = main_start.checked_add(main_len)?; if main_end + 16 > bytes.len() || main_len != 0 && read_u16(bytes, main_end - 2).map(|word| word & 0x3ff) != Some(8) { return None; } let mut blocks = vec![(main_start, main_end)]; let mut at = main_end + 16; while at < bytes.len() { let name_len = usize::from(read_u16(bytes, at + 1)?); let name = bytes.get(at + 3..at + 3 + name_len)?; if bytes.get(at) != Some(&0) || !(1..=40).contains(&name_len) || !name.first().is_some_and(u8::is_ascii_alphabetic) || !name .iter() .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_') { return None; } let code_len = usize::from(read_u16(bytes, at + 6 + name_len)?); let start = at + 8 + name_len; let end = start.checked_add(code_len)?; if code_len & 1 != 0 || end + 16 > bytes.len() || code_len != 0 && read_u16(bytes, end - 2).map(|word| word & 0x3ff) != Some(8) { return None; } blocks.push((start, end)); at = end + 16; } (at == bytes.len()).then_some((symbol_start, main_header, blocks)) } fn module_layout(bytes: &[u8], after_catalog: usize) -> Option { for base in after_catalog..after_catalog.saturating_add(24).min(bytes.len()) { if let Some(layout) = candidate_layout(bytes, base) { return Some(layout); } } None } pub(super) fn decode(file: &str, bytes: &[u8], after_catalog: usize) -> Result { let (symbol_start, symbol_end, blocks) = module_layout(bytes, after_catalog) .ok_or_else(|| error(file, after_catalog, "Codeteil", "unerkannte Struktur"))?; let sym = &bytes[symbol_start..symbol_end]; let mut output = String::new(); let mut pending_reset = 0; for (start, end) in blocks { let (block, block_reset) = Decoder::new(file, sym, &bytes[start..end], start).run()?; if block.is_empty() { pending_reset |= block_reset; continue; } if !output.is_empty() && !block.is_empty() { output.push('\n'); if pending_reset != 0 { output.push_str("DEFSNG "); output.push_str(&letter_ranges(pending_reset)); output.push('\n'); } } output.push_str(&block); pending_reset = block_reset; } Ok(output) } #[cfg(test)] mod tests { use super::*; fn symbols(names: &[&str]) -> (Vec, Vec) { let mut table = Vec::new(); let mut offsets = Vec::new(); for name in names { offsets.push(table.len() as u16); table.extend_from_slice(&[0, 0, 0, name.len() as u8]); table.extend_from_slice(name.as_bytes()); } (table, offsets) } #[test] fn decodes_vbdos_declarations_without_losing_type_or_bounds() { let (sym, ids) = symbols(&["PrepVal", "StringToPrep", "Oldcontents"]); let mut decoder = Decoder::new("test.frm", &sym, &[], 0); let mut proc = Vec::new(); for word in [ids[0], 0x02c7, 1, ids[1], 0x2000, 9, 0] { proc.extend_from_slice(&word.to_le_bytes()); } decoder.pcode = 0x058; assert_eq!( decoder.proc_decl(&proc, 0, true).unwrap(), "FUNCTION PrepVal$ (StringToPrep AS CONTROL)" ); decoder.hparam = 0; assert_eq!( decoder.data_tag("id+", 0, &ids[0].to_le_bytes()).unwrap(), "PrepVal$" ); let mut decoder = Decoder::new("test.frm", &sym, &[], 0); decoder.stack = vec![ Item { kind: "expr".into(), text: "0".into(), }, Item { kind: "expr".into(), text: "6".into(), }, ]; let mut array = Vec::new(); array.extend_from_slice(&2u16.to_le_bytes()); array.extend_from_slice(&ids[2].to_le_bytes()); assert_eq!( decoder.array(&array, true).unwrap(), "Oldcontents({1} TO {0})" ); assert_eq!( decoder.array(&array, false).unwrap(), "Oldcontents({1}, {0})" ); array[..2].copy_from_slice(&0u16.to_le_bytes()); assert_eq!(decoder.array(&array, false).unwrap(), "Oldcontents()"); } #[test] fn next_trennt_eine_vorangehende_anweisung_auch_ohne_newline_token() { let (sym, ids) = symbols(&["i"]); let mut code = Vec::new(); code.extend_from_slice(&0u16.to_le_bytes()); code.extend_from_slice(&0x00au16.to_le_bytes()); code.extend_from_slice(&9u16.to_le_bytes()); code.extend_from_slice(&[0, 0]); code.extend_from_slice(b"PRINT 1"); code.push(0); code.extend_from_slice(&(0x0400u16 | 0x00b).to_le_bytes()); code.extend_from_slice(&ids[0].to_le_bytes()); code.extend_from_slice(&0x066u16.to_le_bytes()); code.extend_from_slice(&[0xff, 0xff, 1, 0]); code.extend_from_slice(&0u16.to_le_bytes()); code.extend_from_slice(&8u16.to_le_bytes()); let (decoded, _) = Decoder::new("test.frm", &sym, &code, 0).run().unwrap(); assert!(decoded.contains("PRINT 1\nNEXT i%\n"), "{decoded:?}"); } #[test] fn decodes_defint_and_vbdos_isam_tokens() { let mut decoder = Decoder::new("test.frm", &[], &[], 0); assert_eq!( decoder .data_tag("defxxx", 0, &[0xff, 0xff, 0xc1, 0xff, 0xff, 0xff]) .unwrap(), "DEFINT A-Z" ); assert!(token_def(0x188) .unwrap() .rules .contains(&"expr::=GETINDEX$({0})")); assert!(token_def(0x181).unwrap().rules.contains(&"expr::=CURDIR$")); assert!(token_def(0x184) .unwrap() .rules .contains(&"expr::=DIR$({0})")); assert!(token_def(0x1c3) .unwrap() .rules .contains(&"expr::=INPUTBOX$({2}, {1}, {0})")); assert!(token_def(0x1a8).unwrap().rules.contains(&"REDIM {0}")); } #[test] fn resets_block_local_default_types_before_the_next_block() { let mut bytes = Vec::new(); bytes.extend_from_slice(&0u16.to_le_bytes()); let first = [ 0x1b, 0x00, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0x00, 0x00, 0x08, 0x00, ]; bytes.extend_from_slice(&(first.len() as u16).to_le_bytes()); bytes.extend_from_slice(&first); bytes.extend_from_slice(&[0; 16]); let second = [ 0x0a, 0x00, 0x03, 0x00, 0x00, 0x00, b'X', 0x00, 0x00, 0x00, 0x08, 0x00, ]; bytes.extend_from_slice(&[0, 1, 0, b'P', 0, 0, 0]); bytes.extend_from_slice(&(second.len() as u16).to_le_bytes()); bytes.extend_from_slice(&second); bytes.extend_from_slice(&[0; 16]); assert_eq!( decode("test.frm", &bytes, 0).unwrap(), "DEFINT A-Z\n\nDEFSNG A-Z\nX\n" ); } }