Laufzeit-Eingabe und Zonenzustand korrigieren und Change archivieren

This commit is contained in:
2026-09-06 08:04:22 +02:00
parent 815825dde7
commit c8b92f0619
36 changed files with 1104 additions and 91 deletions

View File

@@ -30,6 +30,8 @@ pub struct RtState {
pub print: PrintState,
/// Hostreihenfolge bis zur Annahme durch Trap, Formular oder Konsole.
pub eingaben: VecDeque<Eingabe>,
/// Bereits angenommener Rest einer von INPUT$ geteilten Taste.
tastenrest: String,
/// Abbruchwunsch (Strg+Untbr) wurde gemeldet.
pub abbruch: bool,
/// Eingabeende wurde gemeldet.
@@ -52,8 +54,10 @@ pub struct RtState {
/// Versatz (dokumentierte Abweichung). Er wirkt auf die Ortszeit.
pub uhr_offset: i64,
/// Woher der UTC-Versatz kommt. Vorgabe ist die Zone des Rechners;
/// ist sie nicht ermittelbar, steht hier `Unbekannt` und es gilt UTC.
/// bei fehlgeschlagener Auflösung gilt für diese Abfrage UTC.
pub zeitzone: dt::Zeitzone,
/// Auflösung der Systemzone; durch Einbetter auch ohne Systemänderung ersetzbar.
pub zonen_ermittler: fn(i64) -> Option<i32>,
rng: u32,
rnd_last: f32,
pub command: String,
@@ -85,6 +89,7 @@ impl Default for RtState {
isam: Default::default(),
print: PrintState::default(),
eingaben: VecDeque::new(),
tastenrest: String::new(),
abbruch: false,
ende: false,
// Das Vorbild belegt die Makros im Interpreter vor; ein
@@ -97,7 +102,8 @@ impl Default for RtState {
dir_treffer: Vec::new(),
dir_index: 0,
uhr_offset: 0,
zeitzone: dt::Zeitzone::ermitteln(),
zeitzone: dt::Zeitzone::System,
zonen_ermittler: dt::utc_versatz,
// Startzustand des Vorbild-PRNG; die exakte
// PRNG-Kompatibilität ist Aufgabe in PLAN.md Phase 3
// („RND/RANDOMIZE — kompatibler PRNG").
@@ -110,6 +116,32 @@ impl Default for RtState {
}
impl RtState {
/// Ortszeit und Gültigkeit stammen aus derselben aktuellen Zonenauflösung.
pub fn zeitpunkt(&self) -> (i64, bool) {
let utc = dt::jetzt_utc();
let zone = match self.zeitzone {
dt::Zeitzone::System => (self.zonen_ermittler)(utc),
dt::Zeitzone::Fest(n) => Some(n),
dt::Zeitzone::Unbekannt => None,
};
(
utc + i64::from(zone.unwrap_or(0)) + self.uhr_offset,
zone.is_some(),
)
}
fn jetzt_serial(&self) -> f64 {
self.zeitpunkt().0 as f64 / 86_400.0 + dt::serial_aus_ymd(1970, 1, 1)
}
/// Höchstens n Codepoints annehmen, übrige Zeichen vor der nächsten Taste lesen.
pub fn tastenanteil(&mut self, mut taste: String, n: usize) -> String {
if let Some((ende, _)) = taste.char_indices().nth(n) {
self.tastenrest = taste.split_off(ende);
}
taste
}
/// Ereignisse vom Host abholen und einsortieren: Größenänderungen wirken
/// sofort auf den Bildschirm, Abbruch und Eingabeende werden gemerkt,
/// Tastendrücke landen in der Warteschlange für `INKEY$`/`INPUT`.
@@ -207,6 +239,9 @@ impl RtState {
/// Nur bereits gepufferte Tasten annehmen, ohne weitere Hostereignisse
/// abzuholen. Die VM kann zwischen Abholen und Blockieren zustellen.
pub fn gepufferte_taste(&mut self) -> Option<String> {
if !self.tastenrest.is_empty() {
return Some(std::mem::take(&mut self.tastenrest));
}
while let Some(i) = self
.eingaben
.iter()
@@ -496,7 +531,8 @@ pub mod ids {
pub const GRAPHICS_LINE: u16 = 153;
pub const GRAPHICS_PAINT: u16 = 154;
pub const GRAPHICS_VIEW: u16 = 155;
pub const COUNT: u16 = 156;
pub const TIMEZONEKNOWN: u16 = 156;
pub const COUNT: u16 = 157;
}
/// Dispatch-Tabelle in Index-Reihenfolge.
@@ -659,6 +695,7 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
bi_graphics_line,
bi_graphics_paint,
bi_graphics_view,
bi_timezoneknown,
];
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
TABLE
@@ -1470,7 +1507,9 @@ fn bi_seek_stmt(
if pos < 1 {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
st.dateien.get(arg_i32(a, 0)?)?.position = pos as u64;
st.dateien
.get(arg_i32(a, 0)?)?
.position_setzen(pos as u64)?;
Ok(None)
}
@@ -1706,9 +1745,9 @@ fn bi_cv(
};
let bytes: Vec<u8> = text
.chars()
.map(|c| u8::try_from(c as u32).unwrap_or(0))
.collect();
if bytes.len() < breite {
.map(|c| u8::try_from(c as u32).map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL))
.collect::<Result<_, _>>()?;
if bytes.len() != breite {
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
}
Ok(Some(match art {
@@ -2180,18 +2219,18 @@ fn bi_input_s(
if a.len() > 1 {
let nummer = arg_i32(a, 1)?;
let datei = st.dateien.get(nummer)?;
let bytes = datei.bytes_lesen(datei.position, n as usize)?;
return Ok(Some(Value::Str(Rc::from(
String::from_utf8_lossy(&bytes).as_ref(),
))));
return s_ok(datei.zeichen_lesen(n as usize)?);
}
// Blockierend, ohne Echo auf dem Bildschirm.
let mut s = String::new();
for _ in 0..n {
let mut rest = n as usize;
while rest > 0 {
host.present(&st.screen);
let Some(t) = st.naechste_taste(host, true) else {
return Err(RuntimeError(62)); // Input past end of file
};
let t = st.tastenanteil(t, rest);
rest -= t.chars().count();
s.push_str(&t);
}
Ok(Some(Value::Str(Rc::from(s.as_str()))))
@@ -2199,6 +2238,14 @@ fn bi_input_s(
// ---- Sonstiges ------------------------------------------------------------------
fn bi_timezoneknown(
st: &mut RtState,
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Int(if st.zeitpunkt().1 { -1 } else { 0 })))
}
fn bi_timer(
st: &mut RtState,
_: &mut dyn Host,
@@ -2209,7 +2256,7 @@ fn bi_timer(
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs_f64().fract())
.unwrap_or(0.0);
let ganze = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
let ganze = st.zeitpunkt().0.rem_euclid(86_400);
Ok(Some(Value::Sng(ganze as f32 + bruchteil as f32)))
}
@@ -2218,7 +2265,7 @@ fn bi_date(
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let secs = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset);
let secs = st.zeitpunkt().0;
let (y, m, d) = dt::civil_from_days(secs.div_euclid(86_400));
s_ok(format!("{m:02}-{d:02}-{y:04}"))
}
@@ -2228,7 +2275,7 @@ fn bi_time(
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
let s = dt::jetzt_sekunden(st.zeitzone, st.uhr_offset).rem_euclid(86_400);
let s = st.zeitpunkt().0.rem_euclid(86_400);
s_ok(format!(
"{:02}:{:02}:{:02}",
s / 3600,
@@ -2245,7 +2292,7 @@ fn bi_date_set(
let t = arg_str(a, 0)?;
let ziel = dt::datevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
// Nur den Datumsanteil verschieben, die Tageszeit bleibt.
let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
let jetzt = st.jetzt_serial();
let diff_tage = ziel.floor() - jetzt.floor();
st.uhr_offset += (diff_tage * 86_400.0) as i64;
Ok(None)
@@ -2258,7 +2305,7 @@ fn bi_time_set(
) -> Result<Option<Value>, RuntimeError> {
let t = arg_str(a, 0)?;
let ziel = dt::timevalue(&t).ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
let jetzt = dt::jetzt_serial(st.zeitzone, st.uhr_offset);
let jetzt = st.jetzt_serial();
let diff = ziel - (jetzt - jetzt.floor());
st.uhr_offset += (diff * 86_400.0).round() as i64;
Ok(None)
@@ -2269,10 +2316,7 @@ fn bi_now(
_: &mut dyn Host,
_: &mut [Value],
) -> Result<Option<Value>, RuntimeError> {
Ok(Some(Value::Dbl(dt::jetzt_serial(
st.zeitzone,
st.uhr_offset,
))))
Ok(Some(Value::Dbl(st.jetzt_serial())))
}
fn bi_date_serial(
@@ -2762,6 +2806,72 @@ mod tests {
Value::Str(Rc::from(v))
}
#[test]
fn input_s_dispatch_bewahrt_codepointreste_auch_ohne_vm() {
let mut st = RtState::default();
let mut host = CaptureHost::default();
host.ereignis(Ereignis::Taste("\0;".into(), 0));
host.ereignis(Ereignis::Taste("ä😀".into(), 0));
for (n, expected) in [(0, ""), (1, "\0"), (2, ""), (1, "😀")] {
let got = bi_input_s(&mut st, &mut host, &mut [Value::Int(n)]).unwrap();
assert!(matches!(got, Some(Value::Str(s)) if &*s == expected));
}
host.ereignis(Ereignis::Ende);
assert_eq!(
bi_input_s(&mut st, &mut host, &mut [Value::Int(1)]).unwrap_err(),
RuntimeError(62)
);
assert_eq!(
bi_input_s(&mut st, &mut host, &mut [Value::Int(-1)]).unwrap_err(),
RuntimeError(5)
);
}
#[test]
fn zonenquelle_ausfall_und_erholung_gelten_fuer_alle_zeitfunktionen() {
thread_local! {
static ZONE: std::cell::Cell<Option<i32>> = const { std::cell::Cell::new(None) };
}
let mut st = RtState {
zonen_ermittler: |_| ZONE.get(),
uhr_offset: 4321,
..RtState::default()
};
let mut host = CaptureHost::default();
for zone in [None, Some(7200), None, Some(0)] {
ZONE.set(zone);
let before = dt::jetzt_utc();
let (time, known) = st.zeitpunkt();
assert_eq!(known, zone.is_some());
let shift = i64::from(zone.unwrap_or(0)) + st.uhr_offset;
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&time));
assert!(
matches!(bi_timezoneknown(&mut st, &mut host, &mut []).unwrap(), Some(Value::Int(n)) if n == if known { -1 } else { 0 })
);
let Some(Value::Dbl(now)) = bi_now(&mut st, &mut host, &mut []).unwrap() else {
panic!()
};
let seconds = ((now - dt::serial_aus_ymd(1970, 1, 1)) * 86_400.0).round() as i64;
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&seconds));
let Some(Value::Str(date)) = bi_date(&mut st, &mut host, &mut []).unwrap() else {
panic!()
};
let Some(Value::Str(clock)) = bi_time(&mut st, &mut host, &mut []).unwrap() else {
panic!()
};
let date_time = ((dt::datevalue(&date).unwrap() + dt::timevalue(&clock).unwrap()
- dt::serial_aus_ymd(1970, 1, 1))
* 86_400.0)
.round() as i64;
assert!((before + shift..=dt::jetzt_utc() + shift).contains(&date_time));
let Some(Value::Sng(timer)) = bi_timer(&mut st, &mut host, &mut []).unwrap() else {
panic!()
};
assert!((timer as f64 - seconds.rem_euclid(86_400) as f64).abs() < 2.0);
assert_eq!(st.uhr_offset, 4321);
}
}
#[test]
fn stringfunktionen_randfaelle() {
// VAL liest Präfix

View File

@@ -132,8 +132,8 @@ pub enum Zeitzone {
impl Zeitzone {
/// Versatz zu einem Zeitpunkt. Bei `System` ohne ermittelbare Zone
/// fällt der Wert auf 0 (UTC) zurück — die Rückfallebene ist über
/// [`Zeitzone::ermitteln`] erkennbar.
/// fällt der Wert auf 0 (UTC) zurück. BASIC und Einbetter verwenden
/// [`crate::builtins::RtState::zeitpunkt`] für Zeit und aktuellen Status.
pub fn versatz(&self, zeitpunkt: i64) -> i32 {
match self {
Zeitzone::System => utc_versatz(zeitpunkt).unwrap_or(0),
@@ -142,8 +142,9 @@ impl Zeitzone {
}
}
/// Einmal beim Start: `System`, wenn die Zone lesbar ist, sonst
/// `Unbekannt`. Damit ist der Rückfall auf UTC sichtbar statt still.
/// Momentaufnahme: `System`, wenn die Zone lesbar ist, sonst `Unbekannt`.
/// Die Laufzeit behält `System` als Quelle, damit sie nach Ausfällen
/// erneut auflösen kann; `RtState::zeitpunkt` liefert den aktuellen Status.
pub fn ermitteln() -> Zeitzone {
match utc_versatz(jetzt_utc()) {
Some(_) => Zeitzone::System,

View File

@@ -52,7 +52,7 @@ pub struct Feld {
pub struct Datei {
pub modus: Modus,
pub pfad: PathBuf,
datei: File,
datei: BufReader<File>,
/// Recordlänge (`RANDOM`); bei `BINARY` immer 1.
pub reclen: usize,
/// Recordpuffer für `FIELD`/`GET`/`PUT`.
@@ -61,8 +61,8 @@ pub struct Datei {
/// Nächste Position: Datensatz bei `RANDOM`, Byte bei `BINARY`,
/// jeweils 1-basiert.
pub position: u64,
/// Zeilenpuffer für sequenzielles Lesen.
leser: Option<BufReader<File>>,
/// Ein Komma kann auch vor EOF noch ein leeres Feld eröffnen.
leeres_feld: bool,
}
impl Datei {
@@ -82,11 +82,7 @@ impl Datei {
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 datei = BufReader::new(opt.open(pfad).map_err(|e| fehler_am_pfad(e, pfad))?);
let reclen = if modus == Modus::Random {
reclen.max(1)
} else {
@@ -100,13 +96,14 @@ impl Datei {
puffer: vec![b' '; reclen],
felder: Vec::new(),
position: 1,
leser,
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)
@@ -117,8 +114,7 @@ impl Datei {
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())
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()?),
@@ -132,24 +128,143 @@ impl Datei {
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)
self.datei
.get_mut()
.write_all(text.as_bytes())
.map_err(fehler_aus_io)
}
/// Eine Zeile lesen (ohne Zeilenende); `None` = Dateiende.
/// `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 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 {
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);
}
while roh.last() == Some(&b'\n') || roh.last() == Some(&b'\r') {
roh.pop();
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));
}
Ok(Some(String::from_utf8_lossy(&roh).into_owned()))
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.
@@ -179,7 +294,10 @@ impl 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.datei
.get_mut()
.write_all(&puffer)
.map_err(fehler_aus_io)?;
self.puffer = puffer;
self.position = nr + 1;
Ok(())
@@ -211,13 +329,16 @@ impl Datei {
self.datei
.seek(SeekFrom::Start(pos - 1))
.map_err(fehler_aus_io)?;
self.datei.write_all(daten).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.flush().map_err(fehler_aus_io)
self.datei.get_mut().flush().map_err(fehler_aus_io)
}
}
@@ -243,9 +364,6 @@ impl Dateien {
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(())
@@ -478,6 +596,7 @@ pub fn pfad_normieren(p: &str) -> PathBuf {
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
@@ -485,32 +604,39 @@ fn fehler_aus_io(e: std::io::Error) -> RuntimeError {
}
}
/// 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> {
std::fs::remove_file(pfad_normieren(pfad)).map_err(fehler_aus_io)
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));
if !a.exists() {
return Err(RuntimeError(53));
}
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(fehler_aus_io)
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);
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)
std::fs::create_dir(&p).map_err(|e| fehler_am_pfad(e, &p))
}
pub fn verzeichnis_entfernen(pfad: &str) -> Result<(), RuntimeError> {
@@ -613,7 +739,7 @@ pub fn felder_trennen(zeile: &str) -> Vec<String> {
/// 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}\"")
format!("\"{}\"", text.replace('"', "\"\""))
} else {
text.trim().to_string()
}