Files
TerminalBasic/crates/tb-runtime/src/fileio.rs

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