Phase 3 (Kern): Laufzeitbibliothek, Bildschirm und Datei-E/A
Setzt den OpenSpec-Change phase-3-runtime-bildschirm um (77/77 Aufgaben). Abdeckung laut Inventar: 195 implementiert, 37 offen, 53 Non-Feature. Vollstaendigkeits-Inventar - docs/inventar.md mit 285 Eintraegen aus den Themenlisten von bas7advr.hlp und qb45advr.hlp, je mit Status und Fundstelle - crates/tb-frontend/tests/inventar.rs haelt die Tabelle in beide Richtungen gegen den Code; der Abdeckungsstand kann nicht veralten Bruchschritt (Puffer, Host, Korpus) - Cell/TextScreen ziehen ratatui-frei nach tb-runtime::screen; tb-ui behaelt Farbabbildung, Widget und den neuen Terminal-Host - Host wechselt vom Zeichenstrom auf Anzeige des Zellenpuffers plus Ereignisse (Taste, Groessenaenderung, Abbruch) - Korpusvergleich auf getrimmten Bildschirm-Snapshot; die acht bestehenden Sollausgaben blieben dabei unveraendert Groessenunabhaengigkeit - 80x25 ist nirgends mehr eine feste Grenze; Groessenaenderungen waehrend der Ausfuehrung werden zugestellt (Inhalt oben links erhalten, Cursor und VIEW PRINT geklemmt) - Korpusnachweis: dasselbe Programm bei 80x25 und 120x40 mit je eigener Sollausgabe, plus ein Programm mit Groessenwechsel mitten im Lauf Bibliothek - Breite Unicode-Zeichen belegen zwei Zellen (Cursor, POS, Randumbruch) - Bildschirm: CLS, COLOR, LOCATE, WIDTH, VIEW PRINT, SCREEN, CSRLIN, POS - Tastatur: INKEY$, INPUT$, Funktionstasten-Makros (KEY n / LIST / ON) - PRINT USING, LPRINT USING, FORMAT$, SetFormatCC - Mathematik mit kompatiblem PRNG (gleiche Saat, gleiche Folge) - Datum und Zeit mit Serienwerten ab 1899 - Finanzmathematik: FV#, PV#, Pmt#, IPmt#, PPmt#, NPer#, Rate#, NPV#, IRR#, MIRR#, SLN#, SYD#, DDB# - Datei-E/A: sequenziell, RANDOM (Recordpuffer und UDT-Variablen), BINARY, FIELD/LSET/RSET, Statusfunktionen, Dateisystem, MK$/CV - System: ENVIRON, FRE, CLEAR, TRON/TROFF, STACK, ERDEV, ERR-Anweisung Altlasten aus Phase 2 - ON ERROR GOTO auf Modulebene ist aus Prozeduren ansprechbar (prozeduruebergreifender Fixup im Codegenerator) - DATA behaelt seinen Rohtext (Gross-/Kleinschreibung, innerer Leerraum) - Die vier TODO-verify-Zellen der Konvertierungsmatrix sind aufgeloest Vom Inventar aufgedeckte Fehler - Zwoelf Non-Features wies der Compiler entgegen der Phase-1-Spec nicht ab (CALLS, SSEG, POINT, VIEW, COM, PEN, STRIG, STICK, die String*-Routinen, LINE und OPEN "COMn:") -- geschlossen - Drei Gruppen fehlten im urspruenglichen Umfang: Finanzmathematik, Record-Konvertierung, Rest der Dateisystemfunktionen - bas7advr.hlp allein ist keine vollstaendige Quelle; das Inventar bildet die Vereinigung mit qb45advr.hlp Neue Changes - phase-3-isam: schliesst Phase 3 ab (Speicherschicht redb) - phase-3-ortszeit: zieht die UTC-Abweichung zurueck Neue Abhaengigkeit: unicode-width. Dokumentiert: sprachreferenz.md und tbvm-design.md sind TODO-frei, docs/bibliothek.md neu, PLAN.md fortgeschrieben. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,660 @@
|
||||
//! Datei-E/A der Laufzeitbibliothek (sequenziell, Random Access, binär).
|
||||
//! 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.
|
||||
|
||||
// Platzhalter — wird in Phase 3 ausgearbeitet (siehe PLAN.md)
|
||||
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: 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,
|
||||
/// Zeilenpuffer für sequenzielles Lesen.
|
||||
leser: Option<BufReader<File>>,
|
||||
}
|
||||
|
||||
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 = opt.open(pfad).map_err(fehler_aus_io)?;
|
||||
let leser = match modus {
|
||||
Modus::Input => Some(BufReader::new(
|
||||
File::open(pfad).map_err(fehler_aus_io)?,
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
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,
|
||||
leser,
|
||||
})
|
||||
}
|
||||
|
||||
/// Dateigröße in Bytes.
|
||||
pub fn laenge(&self) -> Result<u64, RuntimeError> {
|
||||
self.datei.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 => {
|
||||
let l = self.leser.as_mut().ok_or(RuntimeError(52))?;
|
||||
Ok(l.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
|
||||
.write_all(text.as_bytes())
|
||||
.map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
/// Eine Zeile lesen (ohne Zeilenende); `None` = Dateiende.
|
||||
pub fn zeile_lesen(&mut self) -> Result<Option<String>, RuntimeError> {
|
||||
if self.modus != Modus::Input {
|
||||
return Err(RuntimeError(54));
|
||||
}
|
||||
let l = self.leser.as_mut().ok_or(RuntimeError(52))?;
|
||||
let mut roh = Vec::new();
|
||||
let n = l.read_until(b'\n', &mut roh).map_err(fehler_aus_io)?;
|
||||
if n == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
while roh.last() == Some(&b'\n') || roh.last() == Some(&b'\r') {
|
||||
roh.pop();
|
||||
}
|
||||
Ok(Some(String::from_utf8_lossy(&roh).into_owned()))
|
||||
}
|
||||
|
||||
/// 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.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.write_all(daten).map_err(fehler_aus_io)?;
|
||||
self.position = pos + daten.len() as u64;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schliessen(&mut self) -> Result<(), RuntimeError> {
|
||||
self.datei.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);
|
||||
if modus == Modus::Input && !p.exists() {
|
||||
return Err(RuntimeError(53)); // File not found
|
||||
}
|
||||
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() {
|
||||
NotFound => RuntimeError(53), // File not found
|
||||
PermissionDenied => RuntimeError(70), // Permission denied
|
||||
AlreadyExists => RuntimeError(58), // File already exists
|
||||
_ => RuntimeError(57), // Device I/O error
|
||||
}
|
||||
}
|
||||
|
||||
/// `KILL` — Datei löschen.
|
||||
pub fn loeschen(pfad: &str) -> Result<(), RuntimeError> {
|
||||
std::fs::remove_file(pfad_normieren(pfad)).map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
/// `NAME alt AS neu`.
|
||||
pub fn umbenennen(alt: &str, neu: &str) -> Result<(), RuntimeError> {
|
||||
let (a, n) = (pfad_normieren(alt), pfad_normieren(neu));
|
||||
if !a.exists() {
|
||||
return Err(RuntimeError(53));
|
||||
}
|
||||
if n.exists() {
|
||||
return Err(RuntimeError(58)); // File already exists
|
||||
}
|
||||
std::fs::rename(a, n).map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
/// `MKDIR` / `RMDIR` / `CHDIR`.
|
||||
pub fn verzeichnis_anlegen(pfad: &str) -> Result<(), RuntimeError> {
|
||||
let p = pfad_normieren(pfad);
|
||||
if p.parent().is_some_and(|e| !e.as_os_str().is_empty() && !e.exists()) {
|
||||
return Err(RuntimeError(76)); // Path not found
|
||||
}
|
||||
std::fs::create_dir(p).map_err(fehler_aus_io)
|
||||
}
|
||||
|
||||
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}\"")
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user