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:
2026-09-04 06:54:19 +02:00
parent a6f005d023
commit 52ccbb5848
94 changed files with 10421 additions and 792 deletions

View File

@@ -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);
}
}