795 lines
26 KiB
Rust
795 lines
26 KiB
Rust
//! Datei-E/A: `OPEN`/`CLOSE`, sequenzieller, wahlfreier und binärer Zugriff,
|
|
//! Recordpuffer mit Festtextfeldern, Positions- und Statusfunktionen sowie
|
|
//! die Dateisystem-Anweisungen.
|
|
//!
|
|
//! Feste Strings in Recordpuffern liegen als **UTF-32** (4 Bytes je Zeichen),
|
|
//! damit `STRING * n` unabhängig vom Inhalt genau `4 · n` Bytes belegt und
|
|
//! die Recordlänge konstant bleibt (Entscheidung 2026-09-02). Binärdateien
|
|
//! sind dadurch bewusst nicht mit denen des Vorbilds austauschbar.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::fs::{File, OpenOptions};
|
|
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use crate::errors::RuntimeError;
|
|
|
|
/// Zugriffsart einer offenen Datei.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Modus {
|
|
Input,
|
|
Output,
|
|
Append,
|
|
Random,
|
|
Binary,
|
|
}
|
|
|
|
impl Modus {
|
|
/// Aus der Kurzsyntax `OPEN "I", #1, …`.
|
|
pub fn aus_text(s: &str) -> Option<Modus> {
|
|
match s.trim().to_ascii_uppercase().chars().next()? {
|
|
'I' => Some(Modus::Input),
|
|
'O' => Some(Modus::Output),
|
|
'A' => Some(Modus::Append),
|
|
'R' => Some(Modus::Random),
|
|
'B' => Some(Modus::Binary),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ein Feld des Recordpuffers (`FIELD`): die gebundene Variable und ihr
|
|
/// Bereich im Puffer.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Feld {
|
|
pub referenz: crate::value::VarRef,
|
|
pub start: usize,
|
|
pub laenge: usize,
|
|
}
|
|
|
|
/// Eine offene Datei.
|
|
#[derive(Debug)]
|
|
pub struct Datei {
|
|
pub modus: Modus,
|
|
pub pfad: PathBuf,
|
|
datei: BufReader<File>,
|
|
/// Recordlänge (`RANDOM`); bei `BINARY` immer 1.
|
|
pub reclen: usize,
|
|
/// Recordpuffer für `FIELD`/`GET`/`PUT`.
|
|
pub puffer: Vec<u8>,
|
|
pub felder: Vec<Feld>,
|
|
/// Nächste Position: Datensatz bei `RANDOM`, Byte bei `BINARY`,
|
|
/// jeweils 1-basiert.
|
|
pub position: u64,
|
|
/// Ein Komma kann auch vor EOF noch ein leeres Feld eröffnen.
|
|
leeres_feld: bool,
|
|
}
|
|
|
|
impl Datei {
|
|
fn oeffnen(pfad: &Path, modus: Modus, reclen: usize) -> Result<Datei, RuntimeError> {
|
|
let mut opt = OpenOptions::new();
|
|
match modus {
|
|
Modus::Input => {
|
|
opt.read(true);
|
|
}
|
|
Modus::Output => {
|
|
opt.write(true).create(true).truncate(true);
|
|
}
|
|
Modus::Append => {
|
|
opt.write(true).create(true).append(true);
|
|
}
|
|
Modus::Random | Modus::Binary => {
|
|
opt.read(true).write(true).create(true);
|
|
}
|
|
}
|
|
let datei = BufReader::new(opt.open(pfad).map_err(|e| fehler_am_pfad(e, pfad))?);
|
|
let reclen = if modus == Modus::Random {
|
|
reclen.max(1)
|
|
} else {
|
|
1
|
|
};
|
|
Ok(Datei {
|
|
modus,
|
|
pfad: pfad.to_path_buf(),
|
|
datei,
|
|
reclen,
|
|
puffer: vec![b' '; reclen],
|
|
felder: Vec::new(),
|
|
position: 1,
|
|
leeres_feld: false,
|
|
})
|
|
}
|
|
|
|
/// Dateigröße in Bytes.
|
|
pub fn laenge(&self) -> Result<u64, RuntimeError> {
|
|
self.datei
|
|
.get_ref()
|
|
.metadata()
|
|
.map(|m| m.len())
|
|
.map_err(fehler_aus_io)
|
|
}
|
|
|
|
/// `EOF` — bei sequenziellem Lesen: nichts mehr im Puffer; sonst:
|
|
/// Position hinter dem letzten Datensatz bzw. Byte.
|
|
pub fn eof(&mut self) -> Result<bool, RuntimeError> {
|
|
match self.modus {
|
|
Modus::Input => {
|
|
Ok(!self.leeres_feld && self.datei.fill_buf().map_err(fehler_aus_io)?.is_empty())
|
|
}
|
|
Modus::Random => Ok((self.position - 1) * self.reclen as u64 >= self.laenge()?),
|
|
Modus::Binary => Ok(self.position > self.laenge()?),
|
|
// Beim Schreiben ist das Ende immer erreicht.
|
|
Modus::Output | Modus::Append => Ok(true),
|
|
}
|
|
}
|
|
|
|
/// Text anhängen (`PRINT #`, `WRITE #`).
|
|
pub fn schreiben(&mut self, text: &str) -> Result<(), RuntimeError> {
|
|
if !matches!(self.modus, Modus::Output | Modus::Append) {
|
|
return Err(RuntimeError(54)); // Bad file mode
|
|
}
|
|
self.datei
|
|
.get_mut()
|
|
.write_all(text.as_bytes())
|
|
.map_err(fehler_aus_io)
|
|
}
|
|
|
|
/// `SEEK`: auch den Lesepuffer verwerfen; Position bleibt 1-basiert.
|
|
pub fn position_setzen(&mut self, pos: u64) -> Result<(), RuntimeError> {
|
|
if pos == 0 {
|
|
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
|
}
|
|
if self.modus == Modus::Input {
|
|
self.datei
|
|
.seek(SeekFrom::Start(pos - 1))
|
|
.map_err(fehler_aus_io)?;
|
|
}
|
|
self.position = pos;
|
|
self.leeres_feld = false;
|
|
Ok(())
|
|
}
|
|
|
|
fn byte_lesen(&mut self) -> Result<Option<u8>, RuntimeError> {
|
|
let mut b = [0];
|
|
if self.datei.read(&mut b).map_err(fehler_aus_io)? == 0 {
|
|
return Ok(None);
|
|
}
|
|
self.position += 1;
|
|
self.leeres_feld = false;
|
|
Ok(Some(b[0]))
|
|
}
|
|
|
|
fn crlf_beenden(&mut self, b: u8) -> Result<(), RuntimeError> {
|
|
if b == b'\r' && self.datei.fill_buf().map_err(fehler_aus_io)?.first() == Some(&b'\n') {
|
|
self.byte_lesen()?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Eine Zeile ab dem gemeinsamen Cursor, ohne genau ein Zeilenende.
|
|
pub fn zeile_lesen(&mut self) -> Result<Option<String>, RuntimeError> {
|
|
if self.modus != Modus::Input {
|
|
return Err(RuntimeError(54));
|
|
}
|
|
let mut roh = Vec::new();
|
|
let mut gelesen = false;
|
|
self.leeres_feld = false;
|
|
while let Some(b) = self.byte_lesen()? {
|
|
gelesen = true;
|
|
if matches!(b, b'\r' | b'\n') {
|
|
self.crlf_beenden(b)?;
|
|
break;
|
|
}
|
|
roh.push(b);
|
|
}
|
|
if !gelesen {
|
|
return Ok(None);
|
|
}
|
|
String::from_utf8(roh)
|
|
.map(Some)
|
|
.map_err(|_| RuntimeError(57))
|
|
}
|
|
|
|
/// Nächstes INPUT-#-Feld, ohne nachfolgende Felder oder Zeilen zu verbrauchen.
|
|
pub fn feld_lesen(&mut self) -> Result<String, RuntimeError> {
|
|
if self.modus != Modus::Input {
|
|
return Err(RuntimeError(54));
|
|
}
|
|
let mut roh = Vec::new();
|
|
let mut in_quote = false;
|
|
let mut hatte_quote = false;
|
|
let mut gelesen = std::mem::take(&mut self.leeres_feld);
|
|
while let Some(b) = self.byte_lesen()? {
|
|
gelesen = true;
|
|
match b {
|
|
b'"' if !hatte_quote && roh.iter().all(u8::is_ascii_whitespace) => {
|
|
roh.clear();
|
|
in_quote = true;
|
|
hatte_quote = true;
|
|
}
|
|
b'"' if in_quote => {
|
|
if self.datei.fill_buf().map_err(fehler_aus_io)?.first() == Some(&b'"') {
|
|
self.byte_lesen()?;
|
|
roh.push(b'"');
|
|
} else {
|
|
in_quote = false;
|
|
}
|
|
}
|
|
b',' if !in_quote => {
|
|
self.leeres_feld = true;
|
|
break;
|
|
}
|
|
b'\r' | b'\n' if !in_quote => {
|
|
self.crlf_beenden(b)?;
|
|
break;
|
|
}
|
|
b if hatte_quote && !in_quote && b.is_ascii_whitespace() => {}
|
|
_ => roh.push(b),
|
|
}
|
|
}
|
|
if !gelesen || in_quote {
|
|
return Err(RuntimeError(62));
|
|
}
|
|
let text = String::from_utf8(roh).map_err(|_| RuntimeError(57))?;
|
|
Ok(if hatte_quote {
|
|
text
|
|
} else {
|
|
text.trim().to_owned()
|
|
})
|
|
}
|
|
|
|
/// INPUT$: exakt n UTF-8-Codepoints; beschädigte Daten sind Fehler 57.
|
|
pub fn zeichen_lesen(&mut self, n: usize) -> Result<String, RuntimeError> {
|
|
if !matches!(self.modus, Modus::Input | Modus::Binary) {
|
|
return Err(RuntimeError(54));
|
|
}
|
|
if self.modus == Modus::Binary {
|
|
self.datei
|
|
.seek(SeekFrom::Start(self.position - 1))
|
|
.map_err(fehler_aus_io)?;
|
|
}
|
|
let mut text = String::new();
|
|
for _ in 0..n {
|
|
let mut bytes = [0; 4];
|
|
bytes[0] = self.byte_lesen()?.ok_or(RuntimeError(62))?;
|
|
let breite = match bytes[0] {
|
|
0..=0x7f => 1,
|
|
0xc2..=0xdf => 2,
|
|
0xe0..=0xef => 3,
|
|
0xf0..=0xf4 => 4,
|
|
_ => return Err(RuntimeError(57)),
|
|
};
|
|
for b in &mut bytes[1..breite] {
|
|
*b = self.byte_lesen()?.ok_or(RuntimeError(57))?;
|
|
}
|
|
text.push_str(std::str::from_utf8(&bytes[..breite]).map_err(|_| RuntimeError(57))?);
|
|
}
|
|
Ok(text)
|
|
}
|
|
|
|
/// Datensatz `nr` (1-basiert) in den Puffer lesen.
|
|
pub fn record_lesen(&mut self, nr: u64) -> Result<(), RuntimeError> {
|
|
let offset = (nr - 1) * self.reclen as u64;
|
|
self.datei
|
|
.seek(SeekFrom::Start(offset))
|
|
.map_err(fehler_aus_io)?;
|
|
self.puffer = vec![b' '; self.reclen];
|
|
// Ein unvollständig gelesener Satz bleibt mit Leerzeichen gefüllt.
|
|
let mut gelesen = 0;
|
|
while gelesen < self.reclen {
|
|
match self.datei.read(&mut self.puffer[gelesen..]) {
|
|
Ok(0) => break,
|
|
Ok(n) => gelesen += n,
|
|
Err(e) => return Err(fehler_aus_io(e)),
|
|
}
|
|
}
|
|
self.position = nr + 1;
|
|
Ok(())
|
|
}
|
|
|
|
/// Puffer als Datensatz `nr` schreiben.
|
|
pub fn record_schreiben(&mut self, nr: u64) -> Result<(), RuntimeError> {
|
|
let offset = (nr - 1) * self.reclen as u64;
|
|
self.datei
|
|
.seek(SeekFrom::Start(offset))
|
|
.map_err(fehler_aus_io)?;
|
|
let puffer = std::mem::take(&mut self.puffer);
|
|
self.datei
|
|
.get_mut()
|
|
.write_all(&puffer)
|
|
.map_err(fehler_aus_io)?;
|
|
self.puffer = puffer;
|
|
self.position = nr + 1;
|
|
Ok(())
|
|
}
|
|
|
|
/// `n` Bytes ab Position `pos` (1-basiert) lesen.
|
|
pub fn bytes_lesen(&mut self, pos: u64, n: usize) -> Result<Vec<u8>, RuntimeError> {
|
|
self.datei
|
|
.seek(SeekFrom::Start(pos - 1))
|
|
.map_err(fehler_aus_io)?;
|
|
let mut buf = vec![0u8; n];
|
|
let mut gelesen = 0;
|
|
while gelesen < n {
|
|
match self.datei.read(&mut buf[gelesen..]) {
|
|
Ok(0) => break,
|
|
Ok(k) => gelesen += k,
|
|
Err(e) => return Err(fehler_aus_io(e)),
|
|
}
|
|
}
|
|
if gelesen < n {
|
|
return Err(RuntimeError(62)); // Input past end of file
|
|
}
|
|
self.position = pos + n as u64;
|
|
Ok(buf)
|
|
}
|
|
|
|
/// Bytes ab Position `pos` (1-basiert) schreiben.
|
|
pub fn bytes_schreiben(&mut self, pos: u64, daten: &[u8]) -> Result<(), RuntimeError> {
|
|
self.datei
|
|
.seek(SeekFrom::Start(pos - 1))
|
|
.map_err(fehler_aus_io)?;
|
|
self.datei
|
|
.get_mut()
|
|
.write_all(daten)
|
|
.map_err(fehler_aus_io)?;
|
|
self.position = pos + daten.len() as u64;
|
|
Ok(())
|
|
}
|
|
|
|
fn schliessen(&mut self) -> Result<(), RuntimeError> {
|
|
self.datei.get_mut().flush().map_err(fehler_aus_io)
|
|
}
|
|
}
|
|
|
|
/// Tabelle der offenen Dateien, nach Dateinummer.
|
|
#[derive(Debug, Default)]
|
|
pub struct Dateien {
|
|
offen: BTreeMap<i32, Datei>,
|
|
}
|
|
|
|
impl Dateien {
|
|
/// `OPEN` — Datei an eine Nummer binden.
|
|
pub fn oeffnen(
|
|
&mut self,
|
|
nummer: i32,
|
|
pfad: &str,
|
|
modus: Modus,
|
|
reclen: usize,
|
|
) -> Result<(), RuntimeError> {
|
|
if nummer < 1 {
|
|
return Err(RuntimeError(52)); // Bad file name or number
|
|
}
|
|
if self.offen.contains_key(&nummer) {
|
|
return Err(RuntimeError(55)); // File already open
|
|
}
|
|
let p = pfad_normieren(pfad);
|
|
self.offen
|
|
.insert(nummer, Datei::oeffnen(&p, modus, reclen)?);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get(&mut self, nummer: i32) -> Result<&mut Datei, RuntimeError> {
|
|
self.offen.get_mut(&nummer).ok_or(RuntimeError(52))
|
|
}
|
|
|
|
pub fn ist_offen(&self, nummer: i32) -> bool {
|
|
self.offen.contains_key(&nummer)
|
|
}
|
|
|
|
/// `CLOSE #n` — eine Datei schließen.
|
|
pub fn schliessen(&mut self, nummer: i32) -> Result<(), RuntimeError> {
|
|
match self.offen.remove(&nummer) {
|
|
Some(mut d) => d.schliessen(),
|
|
None => Ok(()), // CLOSE auf eine geschlossene Nummer ist folgenlos
|
|
}
|
|
}
|
|
|
|
/// `CLOSE` / `RESET` — alle Dateien schließen.
|
|
pub fn alle_schliessen(&mut self) -> Result<(), RuntimeError> {
|
|
let nummern: Vec<i32> = self.offen.keys().copied().collect();
|
|
for n in nummern {
|
|
self.schliessen(n)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Zu welcher Datei und welchem Bereich gehört diese Variable?
|
|
pub fn feld_finden(&self, r: &crate::value::VarRef) -> Option<(i32, usize, usize)> {
|
|
for (n, d) in &self.offen {
|
|
for f in &d.felder {
|
|
if gleiche_referenz(&f.referenz, r) {
|
|
return Some((*n, f.start, f.laenge));
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// `FREEFILE` — kleinste freie Nummer.
|
|
pub fn freie_nummer(&self) -> i32 {
|
|
(1..).find(|n| !self.offen.contains_key(n)).unwrap_or(1)
|
|
}
|
|
}
|
|
|
|
/// Zeigen zwei Referenzen auf denselben Speicherort?
|
|
fn gleiche_referenz(a: &crate::value::VarRef, b: &crate::value::VarRef) -> bool {
|
|
use crate::value::VarRef::*;
|
|
match (a, b) {
|
|
(Global(x), Global(y)) => x == y,
|
|
(Stack(x), Stack(y)) => x == y,
|
|
(Elem(x, i), Elem(y, j)) => std::rc::Rc::ptr_eq(x, y) && i == j,
|
|
(Field(x, p), Field(y, q)) => std::rc::Rc::ptr_eq(x, y) && p == q,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
// ---- Recordpuffer: feste Strings als UTF-32 ---------------------------------
|
|
|
|
/// Bytes je Zeichen eines festen Strings im Recordpuffer.
|
|
pub const UTF32_BREITE: usize = 4;
|
|
|
|
/// Festen String in den Puffer schreiben: links- bzw. rechtsbündig,
|
|
/// überlange Werte werden abgeschnitten.
|
|
pub fn feld_setzen(puffer: &mut [u8], start: usize, laenge: usize, wert: &str, rechts: bool) {
|
|
let zeichen: Vec<char> = wert.chars().take(laenge).collect();
|
|
let fehlt = laenge - zeichen.len();
|
|
let mut alle: Vec<char> = Vec::with_capacity(laenge);
|
|
if rechts {
|
|
alle.extend(std::iter::repeat_n(' ', fehlt));
|
|
alle.extend(zeichen);
|
|
} else {
|
|
alle.extend(zeichen);
|
|
alle.extend(std::iter::repeat_n(' ', fehlt));
|
|
}
|
|
for (i, c) in alle.iter().enumerate() {
|
|
let off = start + i * UTF32_BREITE;
|
|
if off + UTF32_BREITE <= puffer.len() {
|
|
puffer[off..off + UTF32_BREITE].copy_from_slice(&(*c as u32).to_le_bytes());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Festen String aus dem Puffer lesen.
|
|
pub fn feld_lesen(puffer: &[u8], start: usize, laenge: usize) -> String {
|
|
let mut s = String::with_capacity(laenge);
|
|
for i in 0..laenge {
|
|
let off = start + i * UTF32_BREITE;
|
|
if off + UTF32_BREITE > puffer.len() {
|
|
break;
|
|
}
|
|
let code = u32::from_le_bytes([
|
|
puffer[off],
|
|
puffer[off + 1],
|
|
puffer[off + 2],
|
|
puffer[off + 3],
|
|
]);
|
|
s.push(char::from_u32(code).unwrap_or(' '));
|
|
}
|
|
s
|
|
}
|
|
|
|
// ---- Werte im Recordpuffer --------------------------------------------------
|
|
|
|
use crate::value::{RecordObj, TypeInit, UdtLayout, Value};
|
|
|
|
/// Bytebreite eines Feldtyps im Recordpuffer. Feste Strings zählen
|
|
/// `UTF32_BREITE` Bytes je Zeichen; Strings variabler Länge sind in Records
|
|
/// nicht zulässig (Vorbild: nur `STRING * n`).
|
|
pub fn breite(t: &TypeInit, layouts: &[UdtLayout]) -> Option<usize> {
|
|
Some(match t {
|
|
TypeInit::Int => 2,
|
|
TypeInit::Lng | TypeInit::Sng => 4,
|
|
TypeInit::Dbl | TypeInit::Cur => 8,
|
|
TypeInit::FixedStr(n) => *n as usize * UTF32_BREITE,
|
|
TypeInit::Udt(i) => {
|
|
let felder = &layouts.get(*i as usize)?.fields;
|
|
let mut summe = 0;
|
|
for f in felder {
|
|
summe += breite(f, layouts)?;
|
|
}
|
|
summe
|
|
}
|
|
TypeInit::Str | TypeInit::Empty => return None,
|
|
})
|
|
}
|
|
|
|
/// Wert in den Puffer schreiben; liefert die belegte Bytezahl.
|
|
pub fn wert_schreiben(
|
|
puffer: &mut [u8],
|
|
offset: usize,
|
|
wert: &Value,
|
|
typ: &TypeInit,
|
|
layouts: &[UdtLayout],
|
|
) -> Result<usize, RuntimeError> {
|
|
let n = breite(typ, layouts).ok_or(RuntimeError::TYPE_MISMATCH)?;
|
|
if offset + n > puffer.len() {
|
|
return Err(RuntimeError(59)); // Bad record length
|
|
}
|
|
match (typ, wert) {
|
|
(TypeInit::Int, v) => puffer[offset..offset + 2]
|
|
.copy_from_slice(&(crate::value::as_f64(v) as i16).to_le_bytes()),
|
|
(TypeInit::Lng, v) => puffer[offset..offset + 4]
|
|
.copy_from_slice(&(crate::value::as_f64(v) as i32).to_le_bytes()),
|
|
(TypeInit::Sng, v) => puffer[offset..offset + 4]
|
|
.copy_from_slice(&(crate::value::as_f64(v) as f32).to_le_bytes()),
|
|
(TypeInit::Dbl, v) => {
|
|
puffer[offset..offset + 8].copy_from_slice(&crate::value::as_f64(v).to_le_bytes())
|
|
}
|
|
(TypeInit::Cur, Value::Cur(c)) => {
|
|
puffer[offset..offset + 8].copy_from_slice(&c.to_le_bytes())
|
|
}
|
|
(TypeInit::Cur, v) => puffer[offset..offset + 8]
|
|
.copy_from_slice(&crate::value::f64_to_cur(crate::value::as_f64(v))?.to_le_bytes()),
|
|
(TypeInit::FixedStr(len), Value::Str(s)) => {
|
|
feld_setzen(puffer, offset, *len as usize, s, false)
|
|
}
|
|
(TypeInit::Udt(i), Value::Rec(r)) => {
|
|
let felder = &layouts
|
|
.get(*i as usize)
|
|
.ok_or(RuntimeError::TYPE_MISMATCH)?
|
|
.fields;
|
|
let rec = r.borrow();
|
|
let mut off = offset;
|
|
for (ft, fv) in felder.iter().zip(rec.fields.iter()) {
|
|
off += wert_schreiben(puffer, off, fv, ft, layouts)?;
|
|
}
|
|
}
|
|
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
|
}
|
|
Ok(n)
|
|
}
|
|
|
|
/// Wert aus dem Puffer lesen.
|
|
pub fn wert_lesen(
|
|
puffer: &[u8],
|
|
offset: usize,
|
|
typ: &TypeInit,
|
|
layouts: &[UdtLayout],
|
|
) -> Result<(Value, usize), RuntimeError> {
|
|
let n = breite(typ, layouts).ok_or(RuntimeError::TYPE_MISMATCH)?;
|
|
if offset + n > puffer.len() {
|
|
return Err(RuntimeError(59));
|
|
}
|
|
let b = &puffer[offset..offset + n];
|
|
let v = match typ {
|
|
TypeInit::Int => Value::Int(i16::from_le_bytes([b[0], b[1]])),
|
|
TypeInit::Lng => Value::Lng(i32::from_le_bytes([b[0], b[1], b[2], b[3]])),
|
|
TypeInit::Sng => Value::Sng(f32::from_le_bytes([b[0], b[1], b[2], b[3]])),
|
|
TypeInit::Dbl => Value::Dbl(f64::from_le_bytes(b[..8].try_into().unwrap())),
|
|
TypeInit::Cur => Value::Cur(i64::from_le_bytes(b[..8].try_into().unwrap())),
|
|
TypeInit::FixedStr(len) => Value::Str(std::rc::Rc::from(
|
|
feld_lesen(puffer, offset, *len as usize).as_str(),
|
|
)),
|
|
TypeInit::Udt(i) => {
|
|
let felder = &layouts
|
|
.get(*i as usize)
|
|
.ok_or(RuntimeError::TYPE_MISMATCH)?
|
|
.fields;
|
|
let mut off = offset;
|
|
let mut werte = Vec::with_capacity(felder.len());
|
|
for ft in felder {
|
|
let (v, k) = wert_lesen(puffer, off, ft, layouts)?;
|
|
werte.push(v);
|
|
off += k;
|
|
}
|
|
Value::Rec(std::rc::Rc::new(std::cell::RefCell::new(RecordObj {
|
|
fields: werte,
|
|
})))
|
|
}
|
|
TypeInit::Str | TypeInit::Empty => return Err(RuntimeError::TYPE_MISMATCH),
|
|
};
|
|
Ok((v, n))
|
|
}
|
|
|
|
// ---- Pfade und Dateisystem --------------------------------------------------
|
|
|
|
/// Pfadtrennzeichen des Vorbilds (`\`) auf die Plattform abbilden.
|
|
pub fn pfad_normieren(p: &str) -> PathBuf {
|
|
if std::path::MAIN_SEPARATOR == '\\' {
|
|
PathBuf::from(p)
|
|
} else {
|
|
PathBuf::from(p.replace('\\', "/"))
|
|
}
|
|
}
|
|
|
|
fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
|
|
use std::io::ErrorKind::*;
|
|
match e.kind() {
|
|
NotADirectory => RuntimeError(76), // Path not found
|
|
NotFound => RuntimeError(53), // File not found
|
|
PermissionDenied => RuntimeError(70), // Permission denied
|
|
AlreadyExists => RuntimeError(58), // File already exists
|
|
_ => RuntimeError(57), // Device I/O error
|
|
}
|
|
}
|
|
|
|
/// NotFound am Dateieintrag (53) gegenüber fehlendem Elternpfad (76).
|
|
fn fehler_am_pfad(e: std::io::Error, pfad: &Path) -> RuntimeError {
|
|
if e.kind() == std::io::ErrorKind::NotFound
|
|
&& pfad
|
|
.parent()
|
|
.is_some_and(|p| !p.as_os_str().is_empty() && !p.is_dir())
|
|
{
|
|
RuntimeError(76)
|
|
} else {
|
|
fehler_aus_io(e)
|
|
}
|
|
}
|
|
|
|
/// `KILL` — Datei löschen.
|
|
pub fn loeschen(pfad: &str) -> Result<(), RuntimeError> {
|
|
let p = pfad_normieren(pfad);
|
|
std::fs::remove_file(&p).map_err(|e| fehler_am_pfad(e, &p))
|
|
}
|
|
|
|
/// `NAME alt AS neu`.
|
|
pub fn umbenennen(alt: &str, neu: &str) -> Result<(), RuntimeError> {
|
|
let (a, n) = (pfad_normieren(alt), pfad_normieren(neu));
|
|
std::fs::metadata(&a).map_err(|e| fehler_am_pfad(e, &a))?;
|
|
if n.exists() {
|
|
return Err(RuntimeError(58)); // File already exists
|
|
}
|
|
std::fs::rename(&a, &n).map_err(|e| fehler_am_pfad(e, &n))
|
|
}
|
|
|
|
/// `MKDIR` / `RMDIR` / `CHDIR`.
|
|
pub fn verzeichnis_anlegen(pfad: &str) -> Result<(), RuntimeError> {
|
|
let p = pfad_normieren(pfad);
|
|
std::fs::create_dir(&p).map_err(|e| fehler_am_pfad(e, &p))
|
|
}
|
|
|
|
pub fn verzeichnis_entfernen(pfad: &str) -> Result<(), RuntimeError> {
|
|
std::fs::remove_dir(pfad_normieren(pfad)).map_err(|e| match e.kind() {
|
|
std::io::ErrorKind::NotFound => RuntimeError(76),
|
|
_ => fehler_aus_io(e),
|
|
})
|
|
}
|
|
|
|
pub fn verzeichnis_wechseln(pfad: &str) -> Result<(), RuntimeError> {
|
|
std::env::set_current_dir(pfad_normieren(pfad)).map_err(|e| match e.kind() {
|
|
std::io::ErrorKind::NotFound => RuntimeError(76),
|
|
_ => fehler_aus_io(e),
|
|
})
|
|
}
|
|
|
|
/// `CURDIR$`.
|
|
pub fn aktuelles_verzeichnis() -> String {
|
|
std::env::current_dir()
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Verzeichnissuche für `DIR$` und `FILES`: alle Namen, die auf das Muster
|
|
/// passen (`*` und `?` wie beim Vorbild).
|
|
pub fn suchen(muster: &str) -> Result<Vec<String>, RuntimeError> {
|
|
let p = pfad_normieren(muster);
|
|
let (verzeichnis, maske) = match p.file_name() {
|
|
Some(n) => (
|
|
p.parent()
|
|
.filter(|e| !e.as_os_str().is_empty())
|
|
.map(|e| e.to_path_buf())
|
|
.unwrap_or_else(|| PathBuf::from(".")),
|
|
n.to_string_lossy().into_owned(),
|
|
),
|
|
None => (p.clone(), "*".to_string()),
|
|
};
|
|
let eintraege = std::fs::read_dir(&verzeichnis).map_err(|e| match e.kind() {
|
|
std::io::ErrorKind::NotFound => RuntimeError(76),
|
|
_ => fehler_aus_io(e),
|
|
})?;
|
|
let mut namen: Vec<String> = eintraege
|
|
.filter_map(|e| e.ok())
|
|
.map(|e| e.file_name().to_string_lossy().into_owned())
|
|
.filter(|n| passt(n, &maske))
|
|
.collect();
|
|
namen.sort();
|
|
Ok(namen)
|
|
}
|
|
|
|
/// Musterabgleich mit `*` (beliebig viele Zeichen) und `?` (genau eines).
|
|
pub fn passt(name: &str, muster: &str) -> bool {
|
|
let n: Vec<char> = name.chars().collect();
|
|
let m: Vec<char> = muster.chars().collect();
|
|
fn rek(n: &[char], m: &[char]) -> bool {
|
|
match m.first() {
|
|
None => n.is_empty(),
|
|
Some('*') => rek(n, &m[1..]) || (!n.is_empty() && rek(&n[1..], m)),
|
|
Some('?') => !n.is_empty() && rek(&n[1..], &m[1..]),
|
|
Some(c) => !n.is_empty() && n[0].eq_ignore_ascii_case(c) && rek(&n[1..], &m[1..]),
|
|
}
|
|
}
|
|
rek(&n, &m)
|
|
}
|
|
|
|
/// Elemente einer `INPUT #`-Zeile trennen: Komma trennt, Anführungszeichen
|
|
/// klammern, Leerraum am Rand fällt weg.
|
|
pub fn felder_trennen(zeile: &str) -> Vec<String> {
|
|
let mut out = Vec::new();
|
|
let mut aktuell = String::new();
|
|
let mut in_quote = false;
|
|
let mut hatte_quote = false;
|
|
for c in zeile.chars() {
|
|
match c {
|
|
'"' if !in_quote && aktuell.trim().is_empty() => {
|
|
in_quote = true;
|
|
hatte_quote = true;
|
|
aktuell.clear();
|
|
}
|
|
'"' if in_quote => in_quote = false,
|
|
',' if !in_quote => {
|
|
out.push(if hatte_quote {
|
|
std::mem::take(&mut aktuell)
|
|
} else {
|
|
std::mem::take(&mut aktuell).trim().to_string()
|
|
});
|
|
hatte_quote = false;
|
|
}
|
|
_ => aktuell.push(c),
|
|
}
|
|
}
|
|
out.push(if hatte_quote {
|
|
aktuell
|
|
} else {
|
|
aktuell.trim().to_string()
|
|
});
|
|
out
|
|
}
|
|
|
|
/// Ein Element für `WRITE #` aufbereiten: Strings in Anführungszeichen.
|
|
pub fn write_element(text: &str, ist_string: bool) -> String {
|
|
if ist_string {
|
|
format!("\"{}\"", text.replace('"', "\"\""))
|
|
} else {
|
|
text.trim().to_string()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn muster_mit_stern_und_fragezeichen() {
|
|
assert!(passt("test.txt", "*.txt"));
|
|
assert!(passt("test.txt", "t?st.txt"));
|
|
assert!(!passt("test.txt", "*.bas"));
|
|
assert!(passt("TEST.TXT", "*.txt")); // Vorbild: Groß/klein egal
|
|
assert!(passt("a", "*"));
|
|
}
|
|
|
|
#[test]
|
|
fn festes_feld_ist_inhaltsunabhaengig_gleich_lang() {
|
|
let mut p = vec![0u8; 10 * UTF32_BREITE];
|
|
feld_setzen(&mut p, 0, 10, "abc", false);
|
|
assert_eq!(feld_lesen(&p, 0, 10), "abc ");
|
|
feld_setzen(&mut p, 0, 10, "äöüßÄÖÜ", false);
|
|
assert_eq!(feld_lesen(&p, 0, 10), "äöüßÄÖÜ ");
|
|
// Der Puffer hat in beiden Fällen dieselbe Länge.
|
|
assert_eq!(p.len(), 40);
|
|
}
|
|
|
|
#[test]
|
|
fn rset_schneidet_ab_und_richtet_rechts_aus() {
|
|
let mut p = vec![0u8; 5 * UTF32_BREITE];
|
|
feld_setzen(&mut p, 0, 5, "ABCDEFG", true);
|
|
assert_eq!(feld_lesen(&p, 0, 5), "ABCDE");
|
|
feld_setzen(&mut p, 0, 5, "xy", true);
|
|
assert_eq!(feld_lesen(&p, 0, 5), " xy");
|
|
}
|
|
|
|
#[test]
|
|
fn felder_einer_input_zeile() {
|
|
assert_eq!(felder_trennen("a,b,c"), vec!["a", "b", "c"]);
|
|
assert_eq!(felder_trennen(" 1 , 2 "), vec!["1", "2"]);
|
|
assert_eq!(felder_trennen("\"a,b\",5"), vec!["a,b", "5"]);
|
|
}
|
|
|
|
#[test]
|
|
fn modus_aus_kurzsyntax() {
|
|
assert_eq!(Modus::aus_text("I"), Some(Modus::Input));
|
|
assert_eq!(Modus::aus_text("output"), Some(Modus::Output));
|
|
assert_eq!(Modus::aus_text("x"), None);
|
|
}
|
|
}
|