//! 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 { 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, /// Recordlänge (`RANDOM`); bei `BINARY` immer 1. pub reclen: usize, /// Recordpuffer für `FIELD`/`GET`/`PUT`. pub puffer: Vec, pub felder: Vec, /// 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 { 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 { 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 { 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, 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, 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 { 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 { 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, 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, } 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 = 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 = wert.chars().take(laenge).collect(); let fehlt = laenge - zeichen.len(); let mut alle: Vec = 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 { 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 { 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, 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 = 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 = name.chars().collect(); let m: Vec = 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 { 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); } }