Rust · 142045 bytes Raw Blame History
1 //! Fortran I/O subsystem — unit management, list-directed and formatted I/O.
2 //!
3 //! The I/O state is global (Fortran I/O units are program-wide). Access
4 //! is protected by a mutex for future thread safety (DO CONCURRENT).
5 //!
6 //! Preconnected units:
7 //! - Unit 5 → stdin
8 //! - Unit 6 → stdout
9 //! - Unit 0 → stderr
10 //! - * in I/O statements → unit 5 (read) or 6 (write)
11
12 use std::collections::HashMap;
13 use std::fs::{File, OpenOptions};
14 use std::io::{self, BufRead, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
15 use std::sync::Mutex;
16
17 // ---- Global I/O state ----
18
19 use std::sync::OnceLock;
20
21 fn io_state() -> &'static Mutex<IoState> {
22 static STATE: OnceLock<Mutex<IoState>> = OnceLock::new();
23 STATE.get_or_init(|| Mutex::new(IoState::new()))
24 }
25
26 fn scratch_filename(unit: i32) -> String {
27 use std::sync::atomic::{AtomicU64, Ordering};
28 static SEQ: AtomicU64 = AtomicU64::new(0);
29 let seq = SEQ.fetch_add(1, Ordering::Relaxed);
30 let pid = std::process::id();
31 let dir = std::env::temp_dir();
32 dir.join(format!(
33 "afs_scratch_{pid}_{}_{seq}.tmp",
34 unit.unsigned_abs()
35 ))
36 .to_string_lossy()
37 .into_owned()
38 }
39
40 #[inline]
41 fn read_i128_ptr(src: *const i128) -> Option<i128> {
42 if src.is_null() {
43 None
44 } else {
45 Some(unsafe { std::ptr::read_unaligned(src) })
46 }
47 }
48
49 #[inline]
50 fn write_i128_ptr(dst: *mut i128, value: i128) {
51 if !dst.is_null() {
52 unsafe { std::ptr::write_unaligned(dst, value) };
53 }
54 }
55
56 // ---- Unit status types ----
57
58 #[derive(Debug, Clone, PartialEq)]
59 enum UnitStatus {
60 Open,
61 }
62
63 #[derive(Debug, Clone, Copy, PartialEq)]
64 enum Access {
65 Sequential,
66 Direct,
67 Stream,
68 }
69
70 #[derive(Debug, Clone, PartialEq)]
71 enum Form {
72 Formatted,
73 Unformatted,
74 }
75
76 #[derive(Debug, Clone, Copy, PartialEq)]
77 enum Action {
78 Read,
79 Write,
80 ReadWrite,
81 }
82
83 // ---- Unit ----
84
85 enum UnitStream {
86 Stdin,
87 Stdout,
88 Stderr,
89 FileRead(BufReader<File>),
90 FileWrite(BufWriter<File>),
91 /// Raw file handle for direct/stream access (supports both read and write + seeking).
92 FileRaw(File),
93 }
94
95 struct Unit {
96 _number: i32,
97 stream: UnitStream,
98 filename: String,
99 _status: UnitStatus,
100 access: Access,
101 form: Form,
102 action: Action,
103 /// Record length for direct access (in bytes). None for sequential/stream.
104 recl: Option<i64>,
105 /// Buffered tokens from the current input record for list-directed READ.
106 read_tokens: Vec<String>,
107 /// Cached formatted input record for the current READ statement.
108 formatted_read_record: Option<String>,
109 /// Cursor within a cached formatted input record for ADVANCE='NO' reads.
110 formatted_read_cursor: usize,
111 /// True for STATUS='SCRATCH' units: backing file is deleted on close or exit.
112 scratch: bool,
113 /// In-flight sequential-unformatted record buffer. Set by
114 /// `afs_list_write_begin` and drained by `afs_list_write_end`.
115 /// While Some, list-directed write helpers append raw bytes here
116 /// instead of writing ASCII or directly to the file stream — the
117 /// whole statement materializes as one record [len][data][len].
118 pending_record: Option<Vec<u8>>,
119 /// In-flight sequential-unformatted record being consumed by a
120 /// list-directed READ statement. Set by `afs_list_read_begin`
121 /// (which reads a full [len][data][len] record into memory) and
122 /// cleared by `afs_list_read_end`. The cursor tracks how many
123 /// bytes the per-item helpers have consumed so far.
124 pending_read: Option<(Vec<u8>, usize)>,
125 }
126
127 impl Unit {
128 fn is_unformatted(&self) -> bool {
129 self.form == Form::Unformatted
130 }
131
132 /// Append raw bytes to the in-flight unformatted record buffer if
133 /// one is open, otherwise write directly to the stream. Returns
134 /// `true` when bytes were buffered.
135 fn raw_or_buffer(&mut self, bytes: &[u8]) -> bool {
136 if let Some(buf) = self.pending_record.as_mut() {
137 buf.extend_from_slice(bytes);
138 true
139 } else {
140 let _ = self.write_raw(bytes);
141 false
142 }
143 }
144
145 /// Consume `n` bytes from the in-flight unformatted read record
146 /// (advancing the cursor). Returns `Some(slice)` when the record
147 /// has enough bytes; `None` when the record is exhausted, when no
148 /// pending record is open, or when fewer than `n` bytes remain.
149 fn read_buffer_take(&mut self, n: usize) -> Option<Vec<u8>> {
150 let (buf, cur) = self.pending_read.as_mut()?;
151 if *cur + n > buf.len() {
152 return None;
153 }
154 let out = buf[*cur..*cur + n].to_vec();
155 *cur += n;
156 Some(out)
157 }
158
159 fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
160 match &mut self.stream {
161 UnitStream::Stdout => {
162 io::stdout().write_all(data)?;
163 }
164 UnitStream::Stderr => {
165 io::stderr().write_all(data)?;
166 }
167 UnitStream::FileWrite(w) => {
168 w.write_all(data)?;
169 }
170 UnitStream::FileRaw(f) => {
171 f.write_all(data)?;
172 }
173 _ => {
174 return Err(io::Error::new(
175 io::ErrorKind::PermissionDenied,
176 "unit not open for writing",
177 ))
178 }
179 }
180 Ok(())
181 }
182
183 fn write_str(&mut self, s: &str) -> io::Result<()> {
184 self.write_bytes(s.as_bytes())
185 }
186
187 fn read_line(&mut self) -> io::Result<String> {
188 let mut line = String::new();
189 match &mut self.stream {
190 UnitStream::Stdin => {
191 io::stdin().lock().read_line(&mut line)?;
192 }
193 UnitStream::FileRead(r) => {
194 r.read_line(&mut line)?;
195 }
196 UnitStream::FileRaw(f) => {
197 let mut byte = [0u8; 1];
198 loop {
199 match f.read(&mut byte)? {
200 0 => break,
201 1 => {
202 line.push(byte[0] as char);
203 if byte[0] == b'\n' {
204 break;
205 }
206 }
207 _ => unreachable!(),
208 }
209 }
210 }
211 _ => {
212 return Err(io::Error::new(
213 io::ErrorKind::PermissionDenied,
214 "unit not open for reading",
215 ))
216 }
217 }
218 Ok(line)
219 }
220
221 /// Get the next token for list-directed READ.
222 /// Reads a new line if the token buffer is empty.
223 fn next_read_token(&mut self) -> io::Result<Option<String>> {
224 // Consume from buffer first.
225 if !self.read_tokens.is_empty() {
226 return Ok(Some(self.read_tokens.remove(0)));
227 }
228 // Read a new line and tokenize.
229 let line = self.read_line()?;
230 let trimmed = line.trim().to_string();
231 if trimmed.is_empty() {
232 return Ok(None); // EOF or blank line
233 }
234 // Split on whitespace and commas.
235 let tokens: Vec<String> = trimmed
236 .split(|c: char| c.is_whitespace() || c == ',')
237 .filter(|s| !s.is_empty())
238 .map(String::from)
239 .collect();
240 if tokens.is_empty() {
241 return Ok(None);
242 }
243 self.read_tokens = tokens;
244 Ok(Some(self.read_tokens.remove(0)))
245 }
246
247 fn flush(&mut self) -> io::Result<()> {
248 match &mut self.stream {
249 UnitStream::Stdout => io::stdout().flush(),
250 UnitStream::Stderr => io::stderr().flush(),
251 UnitStream::FileWrite(w) => w.flush(),
252 UnitStream::FileRaw(f) => f.flush(),
253 _ => Ok(()),
254 }
255 }
256 }
257
258 // ---- I/O State ----
259
260 struct IoState {
261 units: HashMap<i32, Unit>,
262 next_newunit: i32,
263 }
264
265 impl IoState {
266 fn new() -> Self {
267 let mut units = HashMap::new();
268
269 // Preconnected units.
270 units.insert(
271 5,
272 Unit {
273 _number: 5,
274 stream: UnitStream::Stdin,
275 filename: "stdin".into(),
276 _status: UnitStatus::Open,
277 access: Access::Sequential,
278 form: Form::Formatted,
279 action: Action::Read,
280 recl: None,
281 read_tokens: Vec::new(),
282 formatted_read_record: None,
283 formatted_read_cursor: 0,
284 scratch: false,
285 pending_record: None,
286 pending_read: None,
287 },
288 );
289 units.insert(
290 6,
291 Unit {
292 _number: 6,
293 stream: UnitStream::Stdout,
294 filename: "stdout".into(),
295 _status: UnitStatus::Open,
296 access: Access::Sequential,
297 form: Form::Formatted,
298 action: Action::Write,
299 recl: None,
300 read_tokens: Vec::new(),
301 formatted_read_record: None,
302 formatted_read_cursor: 0,
303 scratch: false,
304 pending_record: None,
305 pending_read: None,
306 },
307 );
308 units.insert(
309 0,
310 Unit {
311 _number: 0,
312 stream: UnitStream::Stderr,
313 filename: "stderr".into(),
314 _status: UnitStatus::Open,
315 access: Access::Sequential,
316 form: Form::Formatted,
317 action: Action::Write,
318 recl: None,
319 read_tokens: Vec::new(),
320 formatted_read_record: None,
321 formatted_read_cursor: 0,
322 scratch: false,
323 pending_record: None,
324 pending_read: None,
325 },
326 );
327
328 Self {
329 units,
330 next_newunit: -10,
331 }
332 }
333
334 fn get_unit(&mut self, unit_num: i32) -> Option<&mut Unit> {
335 self.units.get_mut(&unit_num)
336 }
337
338 fn alloc_newunit(&mut self) -> i32 {
339 let u = self.next_newunit;
340 self.next_newunit -= 1;
341 u
342 }
343 }
344
345 // ---- Public C API: OPEN/CLOSE ----
346
347 /// Open a file and associate it with a unit number.
348 /// OPEN control block — packed struct for passing all OPEN specifiers in one pointer.
349 #[repr(C)]
350 pub struct OpenControlBlock {
351 pub unit: i32,
352 pub filename: *const u8,
353 pub filename_len: i64,
354 pub status: *const u8,
355 pub status_len: i64,
356 pub action: *const u8,
357 pub action_len: i64,
358 pub access: *const u8,
359 pub access_len: i64,
360 pub form: *const u8,
361 pub form_len: i64,
362 pub recl: i64,
363 pub iostat: *mut i32,
364 pub newunit: *mut i32,
365 pub position: *const u8,
366 pub position_len: i64,
367 }
368
369 /// Simple OPEN with the most common specifiers (fits in 8 registers).
370 /// Used by the IR lowering for basic OPEN statements.
371 #[no_mangle]
372 pub extern "C" fn afs_open_simple(
373 unit: i32,
374 filename: *const u8,
375 filename_len: i64,
376 status: *const u8,
377 status_len: i64,
378 action: *const u8,
379 action_len: i64,
380 ) {
381 let cb = OpenControlBlock {
382 unit,
383 filename,
384 filename_len,
385 status,
386 status_len,
387 action,
388 action_len,
389 access: std::ptr::null(),
390 access_len: 0,
391 form: std::ptr::null(),
392 form_len: 0,
393 recl: 0,
394 iostat: std::ptr::null_mut(),
395 newunit: std::ptr::null_mut(),
396 position: std::ptr::null(),
397 position_len: 0,
398 };
399 afs_open(&cb);
400 }
401
402 /// Open a file unit. Takes a pointer to an OpenControlBlock to avoid
403 /// exceeding the 8-register ARM64 calling convention limit.
404 #[no_mangle]
405 pub extern "C" fn afs_open(cb: *const OpenControlBlock) {
406 if cb.is_null() {
407 return;
408 }
409 let cb = unsafe { &*cb };
410 let unit = cb.unit;
411 let fname = unsafe_str(cb.filename, cb.filename_len);
412 let status_str = unsafe_str(cb.status, cb.status_len).to_lowercase();
413 let is_scratch = status_str.trim() == "scratch";
414 let action_str = unsafe_str(cb.action, cb.action_len).to_lowercase();
415 let access_str = unsafe_str(cb.access, cb.access_len).to_lowercase();
416 let form_str = unsafe_str(cb.form, cb.form_len).to_lowercase();
417 let position_str = unsafe_str(cb.position, cb.position_len).to_lowercase();
418 let recl = cb.recl;
419 let iostat = cb.iostat;
420 let newunit = cb.newunit;
421 let missing_filename = fname.trim().is_empty();
422
423 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
424
425 // NEWUNIT: allocate a new unit number.
426 let actual_unit = if !newunit.is_null() {
427 let u = state.alloc_newunit();
428 unsafe {
429 *newunit = u;
430 }
431 u
432 } else {
433 unit
434 };
435
436 let existing_unit = state.units.get(&actual_unit).map(|u| {
437 (
438 u.filename.clone(),
439 u.access,
440 u.form.clone(),
441 u.action,
442 u.recl,
443 )
444 });
445 let fname = if missing_filename {
446 if is_scratch {
447 // STATUS='SCRATCH': F2018 §12.5.6.13 — implementation chooses the
448 // backing path; file must not be FILE=, must be deleted on close.
449 scratch_filename(actual_unit)
450 } else {
451 existing_unit
452 .as_ref()
453 .map(|(filename, _, _, _, _)| filename.clone())
454 .unwrap_or(fname)
455 }
456 } else {
457 fname
458 };
459
460 let update_existing_in_place = missing_filename
461 && existing_unit.is_some()
462 && status_str.trim().is_empty()
463 && action_str.trim().is_empty()
464 && access_str.trim().is_empty()
465 && form_str.trim().is_empty()
466 && recl <= 0
467 && newunit.is_null();
468 if update_existing_in_place {
469 if let Some(unit) = state.get_unit(actual_unit) {
470 match position_str.trim() {
471 "append" => match &mut unit.stream {
472 UnitStream::FileRaw(f) => {
473 let _ = f.seek(SeekFrom::End(0));
474 }
475 UnitStream::FileRead(r) => {
476 let _ = r.seek(SeekFrom::End(0));
477 }
478 UnitStream::FileWrite(w) => {
479 let _ = w.seek(SeekFrom::End(0));
480 }
481 _ => {}
482 },
483 "rewind" => match &mut unit.stream {
484 UnitStream::FileRaw(f) => {
485 let _ = f.seek(SeekFrom::Start(0));
486 }
487 UnitStream::FileRead(r) => {
488 let _ = r.seek(SeekFrom::Start(0));
489 }
490 UnitStream::FileWrite(w) => {
491 let _ = w.seek(SeekFrom::Start(0));
492 }
493 _ => {}
494 },
495 _ => {}
496 }
497 }
498 if !iostat.is_null() {
499 unsafe {
500 *iostat = 0;
501 }
502 }
503 return;
504 }
505
506 // Build OpenOptions based on status/action.
507 let mut opts = OpenOptions::new();
508 match status_str.trim() {
509 "old" => {
510 opts.read(true);
511 }
512 "new" => {
513 opts.write(true).create_new(true);
514 }
515 "replace" => {
516 opts.write(true).create(true).truncate(true);
517 }
518 "scratch" | "unknown" | "" => {
519 opts.read(true).write(true).create(true);
520 }
521 _ => {
522 opts.read(true).write(true).create(true);
523 }
524 }
525
526 // Determine action. Default depends on status:
527 // old → read, new/replace → write, scratch/unknown → readwrite.
528 let effective_action = match action_str.trim() {
529 "read" => "read",
530 "write" => "write",
531 "readwrite" => "readwrite",
532 "" => existing_unit
533 .as_ref()
534 .map(|(_, _, _, action, _)| match action {
535 Action::Read => "read",
536 Action::Write => "write",
537 Action::ReadWrite => "readwrite",
538 })
539 .unwrap_or_else(|| match status_str.trim() {
540 "old" => "read",
541 "new" | "replace" => "write",
542 _ => "readwrite",
543 }),
544 _ => "readwrite",
545 };
546
547 match effective_action {
548 "read" => {
549 opts.read(true);
550 }
551 "write" => {
552 opts.write(true).create(true);
553 }
554 _ => {
555 opts.read(true).write(true).create(true);
556 }
557 }
558
559 // Flush and close existing unit if re-opening.
560 if let Some(mut existing) = state.units.remove(&actual_unit) {
561 let _ = existing.flush();
562 // Drop closes the file handle.
563 }
564
565 match opts.open(&fname) {
566 Ok(file) => {
567 let file_action = match effective_action {
568 "read" => Action::Read,
569 "write" => Action::Write,
570 _ => Action::ReadWrite,
571 };
572 let file_access = match access_str.trim() {
573 "direct" => Access::Direct,
574 "stream" => Access::Stream,
575 "" => existing_unit
576 .as_ref()
577 .map(|(_, access, _, _, _)| *access)
578 .unwrap_or(Access::Sequential),
579 _ => Access::Sequential,
580 };
581 let file_form = match form_str.trim() {
582 "unformatted" => Form::Unformatted,
583 "" => existing_unit
584 .as_ref()
585 .map(|(_, _, form, _, _)| form.clone())
586 .unwrap_or(Form::Formatted),
587 _ => Form::Formatted,
588 };
589
590 // Direct, stream, and readwrite access use raw file handle for seeking.
591 // ReadWrite needs FileRaw because BufWriter can't read and BufReader can't write.
592 let stream = match file_access {
593 Access::Direct | Access::Stream => UnitStream::FileRaw(file),
594 Access::Sequential => match file_action {
595 Action::Read => UnitStream::FileRead(BufReader::new(file)),
596 Action::Write => UnitStream::FileWrite(BufWriter::new(file)),
597 Action::ReadWrite => UnitStream::FileRaw(file),
598 },
599 };
600 state.units.insert(
601 actual_unit,
602 Unit {
603 _number: actual_unit,
604 stream,
605 filename: fname,
606 _status: UnitStatus::Open,
607 access: file_access,
608 form: file_form,
609 action: file_action,
610 recl: if recl > 0 {
611 Some(recl)
612 } else {
613 existing_unit
614 .as_ref()
615 .and_then(|(_, _, _, _, existing_recl)| *existing_recl)
616 },
617 read_tokens: Vec::new(),
618 formatted_read_record: None,
619 formatted_read_cursor: 0,
620 scratch: is_scratch,
621 pending_record: None,
622 pending_read: None,
623 },
624 );
625
626 // Apply POSITION specifier.
627 // Default: REWIND for sequential, ASIS for direct/stream.
628 let pos = match position_str.trim() {
629 "append" => Some(SeekFrom::End(0)),
630 "rewind" => Some(SeekFrom::Start(0)),
631 "asis" => None,
632 "" => match file_access {
633 Access::Sequential => Some(SeekFrom::Start(0)),
634 _ => None,
635 },
636 _ => None,
637 };
638 if let Some(seek) = pos {
639 if let Some(u) = state.get_unit(actual_unit) {
640 match &mut u.stream {
641 UnitStream::FileRaw(f) => {
642 let _ = f.seek(seek);
643 }
644 UnitStream::FileRead(r) => {
645 let _ = r.seek(seek);
646 }
647 UnitStream::FileWrite(w) => {
648 let _ = w.seek(seek);
649 }
650 _ => {}
651 }
652 }
653 }
654
655 if !iostat.is_null() {
656 unsafe {
657 *iostat = 0;
658 }
659 }
660 }
661 Err(e) => {
662 if !iostat.is_null() {
663 unsafe {
664 *iostat = e.raw_os_error().unwrap_or(1);
665 }
666 } else {
667 // Release the io_state mutex before exit. process::exit invokes
668 // libc atexit handlers — including afs_io_finalize, which locks
669 // io_state to flush units. Holding the lock here while exiting
670 // deadlocked on macOS where the atexit thread re-entered the
671 // same mutex (sample-trace: pthread_mutex_firstfit_lock_wait
672 // → __psynch_mutexwait, hangs forever).
673 drop(state);
674 eprintln!("OPEN: {}: {}", fname, e);
675 std::process::exit(1);
676 }
677 }
678 }
679 }
680
681 /// Close a unit.
682 #[no_mangle]
683 pub extern "C" fn afs_close(unit: i32, iostat: *mut i32) {
684 afs_close_ex(unit, std::ptr::null(), 0, iostat);
685 }
686
687 /// Close a unit with optional STATUS= semantics.
688 #[no_mangle]
689 pub extern "C" fn afs_close_ex(unit: i32, status: *const u8, status_len: i64, iostat: *mut i32) {
690 let delete_on_close = if status.is_null() || status_len <= 0 {
691 false
692 } else {
693 let raw = unsafe { std::slice::from_raw_parts(status, status_len as usize) };
694 std::str::from_utf8(raw)
695 .map(|s| s.trim().eq_ignore_ascii_case("delete"))
696 .unwrap_or(false)
697 };
698
699 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
700 if let Some(mut u) = state.units.remove(&unit) {
701 let _ = u.flush();
702 let filename = u.filename.clone();
703 // STATUS='SCRATCH' units always delete on close (F2018 §12.5.6.13).
704 let delete = delete_on_close || u.scratch;
705 drop(u);
706
707 let mut close_status = 0;
708 if delete
709 && !matches!(filename.as_str(), "stdin" | "stdout" | "stderr")
710 && !filename.is_empty()
711 {
712 if let Err(e) = std::fs::remove_file(&filename) {
713 close_status = e.raw_os_error().unwrap_or(1);
714 }
715 }
716
717 if !iostat.is_null() {
718 unsafe { *iostat = close_status };
719 } else if close_status != 0 {
720 // Release lock before exit (afs_io_finalize atexit re-locks). See afs_open.
721 drop(state);
722 eprintln!(
723 "CLOSE: {}: {}",
724 filename,
725 io::Error::from_raw_os_error(close_status)
726 );
727 std::process::exit(1);
728 }
729 } else {
730 if !iostat.is_null() {
731 unsafe {
732 *iostat = 0;
733 }
734 } // closing unopen unit is not an error
735 }
736 }
737
738 // ---- Public C API: List-directed WRITE ----
739
740 /// Write an integer value (list-directed).
741 #[no_mangle]
742 pub extern "C" fn afs_write_int(unit: i32, val: i32) {
743 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
744 if let Some(u) = state.get_unit(unit) {
745 if u.is_unformatted() {
746 u.raw_or_buffer(&val.to_ne_bytes());
747 } else {
748 let _ = u.write_str(&format!(" {}", val));
749 }
750 }
751 }
752
753 /// Write a 64-bit integer value (list-directed).
754 #[no_mangle]
755 pub extern "C" fn afs_write_int64(unit: i32, val: i64) {
756 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
757 if let Some(u) = state.get_unit(unit) {
758 if u.is_unformatted() {
759 u.raw_or_buffer(&val.to_ne_bytes());
760 } else {
761 let _ = u.write_str(&format!(" {}", val));
762 }
763 }
764 }
765
766 /// Write a 128-bit integer value (list-directed).
767 #[no_mangle]
768 pub extern "C" fn afs_write_int128(unit: i32, val: i128) {
769 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
770 if let Some(u) = state.get_unit(unit) {
771 if u.is_unformatted() {
772 u.raw_or_buffer(&val.to_ne_bytes());
773 } else {
774 let _ = u.write_str(&format!(" {}", val));
775 }
776 }
777 }
778
779 /// Write a real value (list-directed).
780 #[no_mangle]
781 pub extern "C" fn afs_write_real(unit: i32, val: f32) {
782 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
783 if let Some(u) = state.get_unit(unit) {
784 if u.is_unformatted() {
785 u.raw_or_buffer(&val.to_ne_bytes());
786 } else {
787 let _ = u.write_str(&format!(" {:14.7E}", val));
788 }
789 }
790 }
791
792 /// Write a double value (list-directed).
793 #[no_mangle]
794 pub extern "C" fn afs_write_real64(unit: i32, val: f64) {
795 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
796 if let Some(u) = state.get_unit(unit) {
797 if u.is_unformatted() {
798 u.raw_or_buffer(&val.to_ne_bytes());
799 } else {
800 let _ = u.write_str(&format!(" {:22.15E}", val));
801 }
802 }
803 }
804
805 /// Write a complex(4) value (list-directed): " (re,im)".
806 /// `ptr` points to a two-element f32 array [real, imag].
807 #[no_mangle]
808 pub extern "C" fn afs_write_complex_f32(unit: i32, ptr: *const f32) {
809 let (re, im) = unsafe { (*ptr, *ptr.add(1)) };
810 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
811 if let Some(u) = state.get_unit(unit) {
812 if u.is_unformatted() {
813 u.raw_or_buffer(&re.to_ne_bytes());
814 u.raw_or_buffer(&im.to_ne_bytes());
815 } else {
816 let _ = u.write_str(&format!(" ({:14.7E},{:14.7E})", re, im));
817 }
818 }
819 }
820
821 /// Write a complex(8) value (list-directed): " (re,im)".
822 /// `ptr` points to a two-element f64 array [real, imag].
823 #[no_mangle]
824 pub extern "C" fn afs_write_complex_f64(unit: i32, ptr: *const f64) {
825 let (re, im) = unsafe { (*ptr, *ptr.add(1)) };
826 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
827 if let Some(u) = state.get_unit(unit) {
828 if u.is_unformatted() {
829 u.raw_or_buffer(&re.to_ne_bytes());
830 u.raw_or_buffer(&im.to_ne_bytes());
831 } else {
832 let _ = u.write_str(&format!(" ({:22.15E},{:22.15E})", re, im));
833 }
834 }
835 }
836
837 /// Write a character string (list-directed).
838 #[no_mangle]
839 pub extern "C" fn afs_write_string(unit: i32, ptr: *const u8, len: i64) {
840 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
841 if let Some(u) = state.get_unit(unit) {
842 if u.is_unformatted() {
843 if !ptr.is_null() && len > 0 {
844 let slice = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
845 u.raw_or_buffer(slice);
846 }
847 } else {
848 let _ = u.write_str(" ");
849 if !ptr.is_null() && len > 0 {
850 let slice = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
851 let _ = u.write_bytes(slice);
852 }
853 }
854 }
855 }
856
857 /// Write a logical value (list-directed).
858 #[no_mangle]
859 pub extern "C" fn afs_write_logical(unit: i32, val: i32) {
860 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
861 if let Some(u) = state.get_unit(unit) {
862 if u.is_unformatted() {
863 u.raw_or_buffer(&val.to_ne_bytes());
864 } else {
865 let _ = u.write_str(if val != 0 { " T" } else { " F" });
866 }
867 }
868 }
869
870 /// End a write statement (newline).
871 #[no_mangle]
872 pub extern "C" fn afs_write_newline(unit: i32) {
873 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
874 if let Some(u) = state.get_unit(unit) {
875 if u.is_unformatted() {
876 // Sequential unformatted: a pending record buffer means
877 // afs_list_write_end was not called — flush nothing here.
878 // Stream unformatted has no record terminator.
879 let _ = u.flush();
880 return;
881 }
882 let _ = u.write_str("\n");
883 let _ = u.flush();
884 }
885 }
886
887 /// Like `afs_write_newline` but no-ops when `advance == 0`. The
888 /// lowering uses this when `advance=` is a runtime-evaluated string
889 /// (e.g. `advance=optval(adv, 'YES')`) — `advance` is precomputed by
890 /// `afs_advance_eval` to 0 (no advance) or 1 (advance).
891 #[no_mangle]
892 pub extern "C" fn afs_write_newline_if(unit: i32, advance: i32) {
893 if advance == 0 {
894 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
895 if let Some(u) = state.get_unit(unit) {
896 let _ = u.flush();
897 }
898 return;
899 }
900 afs_write_newline(unit);
901 }
902
903 /// Evaluate an `advance=` string at runtime. Returns 0 when the
904 /// trimmed, case-folded string equals "no", else 1. The lowering
905 /// uses this for non-literal advance expressions so that
906 /// `advance=optval(adv, 'YES')` produces the correct behavior
907 /// (current lowering only honors string-literal advance values).
908 #[no_mangle]
909 pub extern "C" fn afs_advance_eval(ptr: *const u8, len: i64) -> i32 {
910 if ptr.is_null() || len <= 0 {
911 return 1;
912 }
913 let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
914 let s = std::str::from_utf8(bytes).unwrap_or("");
915 if s.trim().eq_ignore_ascii_case("no") {
916 0
917 } else {
918 1
919 }
920 }
921
922 /// Read a formatted character item with runtime advance dispatch.
923 /// `advance == 0` selects the non-advancing path
924 /// (`afs_fmt_read_string_noadvance`); any other value uses
925 /// `afs_fmt_read_string` which advances past the record. Used by the
926 /// lowering when `advance=` is a non-literal expression and the bool
927 /// path can't be statically chosen.
928 #[no_mangle]
929 pub extern "C" fn afs_fmt_read_string_dyn(
930 unit: i32,
931 fmt_str: *const u8,
932 fmt_len: i64,
933 data_index: i64,
934 dest: *mut u8,
935 dest_len: i64,
936 size_out: *mut i32,
937 iostat: *mut i32,
938 advance: i32,
939 ) {
940 if advance == 0 {
941 afs_fmt_read_string_noadvance(unit, fmt_str, fmt_len, dest, dest_len, size_out, iostat);
942 } else {
943 afs_fmt_read_string(
944 unit, fmt_str, fmt_len, data_index, dest, dest_len, size_out, iostat,
945 );
946 }
947 }
948
949 /// Begin a list-directed write statement. Mandatory before the first
950 /// per-item helper when iostat=/iomsg= are requested or when the unit
951 /// might be sequential-unformatted (which needs record-buffered emit).
952 ///
953 /// For formatted units this only resets iostat. For sequential
954 /// unformatted units it opens a fresh per-statement record buffer that
955 /// the per-item helpers will append into. Stream-unformatted units skip
956 /// the buffer (each helper writes raw bytes directly).
957 #[no_mangle]
958 pub extern "C" fn afs_list_write_begin(
959 unit: i32,
960 iostat: *mut i32,
961 iomsg: *mut u8,
962 iomsg_len: i64,
963 ) {
964 if !iostat.is_null() {
965 unsafe {
966 *iostat = 0;
967 }
968 }
969 if !iomsg.is_null() && iomsg_len > 0 {
970 let buf = unsafe { std::slice::from_raw_parts_mut(iomsg, iomsg_len as usize) };
971 for b in buf.iter_mut() {
972 *b = b' ';
973 }
974 }
975 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
976 if let Some(u) = state.get_unit(unit) {
977 if u.form == Form::Unformatted && u.access == Access::Sequential {
978 u.pending_record = Some(Vec::new());
979 }
980 } else if !iostat.is_null() {
981 unsafe {
982 *iostat = 1;
983 }
984 }
985 }
986
987 /// End a list-directed write statement. For sequential unformatted
988 /// units this drains the per-statement record buffer and writes
989 /// `[len][bytes][len]` to the stream. For formatted units the trailing
990 /// newline is left to the per-item path's `afs_write_newline` so we
991 /// don't double-newline; this only flushes and forwards iostat/iomsg.
992 /// `advance` is accepted for symmetry with `afs_fmt_end` but is unused
993 /// by the formatted path here.
994 #[no_mangle]
995 pub extern "C" fn afs_list_write_end(
996 unit: i32,
997 _advance: i32,
998 iostat: *mut i32,
999 iomsg: *mut u8,
1000 iomsg_len: i64,
1001 ) {
1002 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1003 let Some(u) = state.get_unit(unit) else {
1004 if !iostat.is_null() {
1005 unsafe {
1006 *iostat = 1;
1007 }
1008 }
1009 return;
1010 };
1011 let mut err: Option<String> = None;
1012 if let Some(buf) = u.pending_record.take() {
1013 let len_bytes = (buf.len() as u32).to_ne_bytes();
1014 let r1 = u.write_raw(&len_bytes);
1015 let r2 = if !buf.is_empty() {
1016 u.write_raw(&buf)
1017 } else {
1018 Ok(())
1019 };
1020 let r3 = u.write_raw(&len_bytes);
1021 if let Err(e) = r1.or(r2).or(r3) {
1022 err = Some(e.to_string());
1023 }
1024 }
1025 let _ = u.flush();
1026 if let Some(msg) = err {
1027 if !iostat.is_null() {
1028 unsafe {
1029 *iostat = 1;
1030 }
1031 }
1032 if !iomsg.is_null() && iomsg_len > 0 {
1033 let buf = unsafe { std::slice::from_raw_parts_mut(iomsg, iomsg_len as usize) };
1034 let bytes = msg.as_bytes();
1035 let n = bytes.len().min(buf.len());
1036 buf[..n].copy_from_slice(&bytes[..n]);
1037 for b in buf[n..].iter_mut() {
1038 *b = b' ';
1039 }
1040 }
1041 }
1042 }
1043
1044 // ---- Public C API: List-directed READ ----
1045
1046 /// Read an i32 value (list-directed) from a unit.
1047 /// Uses token buffer: multiple values on one line are consumed left-to-right.
1048 #[no_mangle]
1049 pub extern "C" fn afs_read_int(unit: i32, val: *mut i32, iostat: *mut i32) {
1050 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1051 if let Some(u) = state.get_unit(unit) {
1052 if let Some(bytes) = u.read_buffer_take(4) {
1053 let mut b = [0u8; 4];
1054 b.copy_from_slice(&bytes);
1055 if !val.is_null() {
1056 unsafe {
1057 *val = i32::from_ne_bytes(b);
1058 }
1059 }
1060 if !iostat.is_null() {
1061 unsafe {
1062 *iostat = 0;
1063 }
1064 }
1065 return;
1066 }
1067 match u.next_read_token() {
1068 Ok(Some(token)) => match token.parse::<i32>() {
1069 Ok(v) => {
1070 if !val.is_null() {
1071 unsafe {
1072 *val = v;
1073 }
1074 }
1075 if !iostat.is_null() {
1076 unsafe {
1077 *iostat = 0;
1078 }
1079 }
1080 }
1081 Err(_) => {
1082 if !iostat.is_null() {
1083 unsafe {
1084 *iostat = 1;
1085 }
1086 } else {
1087 eprintln!("READ: cannot parse integer from '{}'", token);
1088 std::process::exit(1);
1089 }
1090 }
1091 },
1092 Ok(None) => {
1093 if !iostat.is_null() {
1094 unsafe {
1095 *iostat = IOSTAT_END;
1096 }
1097 } else {
1098 eprintln!("READ: end of file");
1099 std::process::exit(1);
1100 }
1101 }
1102 Err(e) => {
1103 if !iostat.is_null() {
1104 unsafe {
1105 *iostat = 1;
1106 }
1107 } else {
1108 eprintln!("READ: {}", e);
1109 std::process::exit(1);
1110 }
1111 }
1112 }
1113 }
1114 }
1115
1116 /// Read an i64 value (list-directed).
1117 #[no_mangle]
1118 pub extern "C" fn afs_read_int64(unit: i32, val: *mut i64, iostat: *mut i32) {
1119 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1120 if let Some(u) = state.get_unit(unit) {
1121 if let Some(bytes) = u.read_buffer_take(8) {
1122 let mut b = [0u8; 8];
1123 b.copy_from_slice(&bytes);
1124 if !val.is_null() {
1125 unsafe {
1126 *val = i64::from_ne_bytes(b);
1127 }
1128 }
1129 if !iostat.is_null() {
1130 unsafe {
1131 *iostat = 0;
1132 }
1133 }
1134 return;
1135 }
1136 match u.next_read_token() {
1137 Ok(Some(token)) => match token.parse::<i64>() {
1138 Ok(v) => {
1139 if !val.is_null() {
1140 unsafe {
1141 *val = v;
1142 }
1143 }
1144 if !iostat.is_null() {
1145 unsafe {
1146 *iostat = 0;
1147 }
1148 }
1149 }
1150 Err(_) => {
1151 if !iostat.is_null() {
1152 unsafe {
1153 *iostat = 1;
1154 }
1155 } else {
1156 eprintln!("READ: cannot parse integer from '{}'", token);
1157 std::process::exit(1);
1158 }
1159 }
1160 },
1161 Ok(None) => {
1162 if !iostat.is_null() {
1163 unsafe {
1164 *iostat = IOSTAT_END;
1165 }
1166 }
1167 }
1168 Err(_) => {
1169 if !iostat.is_null() {
1170 unsafe {
1171 *iostat = 1;
1172 }
1173 }
1174 }
1175 }
1176 }
1177 }
1178
1179 /// Read an i128 value (list-directed).
1180 #[no_mangle]
1181 pub extern "C" fn afs_read_int128(unit: i32, val: *mut i128, iostat: *mut i32) {
1182 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1183 if let Some(u) = state.get_unit(unit) {
1184 if let Some(bytes) = u.read_buffer_take(16) {
1185 let mut b = [0u8; 16];
1186 b.copy_from_slice(&bytes);
1187 write_i128_ptr(val, i128::from_ne_bytes(b));
1188 if !iostat.is_null() {
1189 unsafe {
1190 *iostat = 0;
1191 }
1192 }
1193 return;
1194 }
1195 match u.next_read_token() {
1196 Ok(Some(token)) => match token.parse::<i128>() {
1197 Ok(v) => {
1198 write_i128_ptr(val, v);
1199 if !iostat.is_null() {
1200 unsafe {
1201 *iostat = 0;
1202 }
1203 }
1204 }
1205 Err(_) => {
1206 if !iostat.is_null() {
1207 unsafe {
1208 *iostat = 1;
1209 }
1210 } else {
1211 eprintln!("READ: cannot parse integer from '{}'", token);
1212 std::process::exit(1);
1213 }
1214 }
1215 },
1216 Ok(None) => {
1217 if !iostat.is_null() {
1218 unsafe {
1219 *iostat = IOSTAT_END;
1220 }
1221 }
1222 }
1223 Err(_) => {
1224 if !iostat.is_null() {
1225 unsafe {
1226 *iostat = 1;
1227 }
1228 }
1229 }
1230 }
1231 }
1232 }
1233
1234 /// Read an f32 value (list-directed).
1235 #[no_mangle]
1236 pub extern "C" fn afs_read_real(unit: i32, val: *mut f32, iostat: *mut i32) {
1237 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1238 if let Some(u) = state.get_unit(unit) {
1239 if let Some(bytes) = u.read_buffer_take(4) {
1240 let mut b = [0u8; 4];
1241 b.copy_from_slice(&bytes);
1242 if !val.is_null() {
1243 unsafe {
1244 *val = f32::from_ne_bytes(b);
1245 }
1246 }
1247 if !iostat.is_null() {
1248 unsafe {
1249 *iostat = 0;
1250 }
1251 }
1252 return;
1253 }
1254 match u.next_read_token() {
1255 Ok(Some(token)) => {
1256 // Handle Fortran D-exponent: replace D with E for parsing.
1257 let normalized = token.replace('d', "e").replace('D', "E");
1258 match normalized.parse::<f32>() {
1259 Ok(v) => {
1260 if !val.is_null() {
1261 unsafe {
1262 *val = v;
1263 }
1264 }
1265 if !iostat.is_null() {
1266 unsafe {
1267 *iostat = 0;
1268 }
1269 }
1270 }
1271 Err(_) => {
1272 if !iostat.is_null() {
1273 unsafe {
1274 *iostat = 1;
1275 }
1276 } else {
1277 eprintln!("READ: cannot parse real from '{}'", token);
1278 std::process::exit(1);
1279 }
1280 }
1281 }
1282 }
1283 Ok(None) => {
1284 if !iostat.is_null() {
1285 unsafe {
1286 *iostat = IOSTAT_END;
1287 }
1288 } else {
1289 eprintln!("READ: end of file");
1290 std::process::exit(1);
1291 }
1292 }
1293 Err(_) => {
1294 if !iostat.is_null() {
1295 unsafe {
1296 *iostat = 1;
1297 }
1298 }
1299 }
1300 }
1301 }
1302 }
1303
1304 /// Read an f64 value (list-directed).
1305 #[no_mangle]
1306 pub extern "C" fn afs_read_real64(unit: i32, val: *mut f64, iostat: *mut i32) {
1307 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1308 if let Some(u) = state.get_unit(unit) {
1309 if let Some(bytes) = u.read_buffer_take(8) {
1310 let mut b = [0u8; 8];
1311 b.copy_from_slice(&bytes);
1312 if !val.is_null() {
1313 unsafe {
1314 *val = f64::from_ne_bytes(b);
1315 }
1316 }
1317 if !iostat.is_null() {
1318 unsafe {
1319 *iostat = 0;
1320 }
1321 }
1322 return;
1323 }
1324 match u.next_read_token() {
1325 Ok(Some(token)) => {
1326 let normalized = token.replace('d', "e").replace('D', "E");
1327 match normalized.parse::<f64>() {
1328 Ok(v) => {
1329 if !val.is_null() {
1330 unsafe {
1331 *val = v;
1332 }
1333 }
1334 if !iostat.is_null() {
1335 unsafe {
1336 *iostat = 0;
1337 }
1338 }
1339 }
1340 Err(_) => {
1341 if !iostat.is_null() {
1342 unsafe {
1343 *iostat = 1;
1344 }
1345 } else {
1346 eprintln!("READ: cannot parse real from '{}'", token);
1347 std::process::exit(1);
1348 }
1349 }
1350 }
1351 }
1352 Ok(None) => {
1353 if !iostat.is_null() {
1354 unsafe {
1355 *iostat = IOSTAT_END;
1356 }
1357 }
1358 }
1359 Err(_) => {
1360 if !iostat.is_null() {
1361 unsafe {
1362 *iostat = 1;
1363 }
1364 }
1365 }
1366 }
1367 }
1368 }
1369
1370 /// Begin a list-directed READ statement. Mandatory before per-item
1371 /// helpers when iostat=/iomsg= are requested or when the unit may be
1372 /// sequential-unformatted (which needs the leading record marker
1373 /// consumed and the data slurped into a buffer for typed take-bytes).
1374 ///
1375 /// For formatted units this only resets iostat. For sequential
1376 /// unformatted units it reads `[u32 len][len bytes][u32 trailer]`,
1377 /// stashes the data in `pending_read`, and the per-item helpers will
1378 /// consume from there. Stream-unformatted reads continue using their
1379 /// existing per-helper raw-byte path.
1380 #[no_mangle]
1381 pub extern "C" fn afs_list_read_begin(
1382 unit: i32,
1383 iostat: *mut i32,
1384 iomsg: *mut u8,
1385 iomsg_len: i64,
1386 ) {
1387 if !iostat.is_null() {
1388 unsafe {
1389 *iostat = 0;
1390 }
1391 }
1392 if !iomsg.is_null() && iomsg_len > 0 {
1393 let buf = unsafe { std::slice::from_raw_parts_mut(iomsg, iomsg_len as usize) };
1394 for b in buf.iter_mut() {
1395 *b = b' ';
1396 }
1397 }
1398 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1399 let Some(u) = state.get_unit(unit) else {
1400 if !iostat.is_null() {
1401 unsafe {
1402 *iostat = 1;
1403 }
1404 }
1405 return;
1406 };
1407 if !(u.form == Form::Unformatted && u.access == Access::Sequential) {
1408 return;
1409 }
1410 let mut len_buf = [0u8; 4];
1411 match u.read_raw(&mut len_buf) {
1412 Ok(4) => {}
1413 Ok(0) => {
1414 if !iostat.is_null() {
1415 unsafe {
1416 *iostat = IOSTAT_END;
1417 }
1418 }
1419 return;
1420 }
1421 _ => {
1422 if !iostat.is_null() {
1423 unsafe {
1424 *iostat = 1;
1425 }
1426 }
1427 return;
1428 }
1429 }
1430 let record_len = u32::from_ne_bytes(len_buf) as usize;
1431 let mut data = vec![0u8; record_len];
1432 if record_len > 0 && u.read_raw(&mut data).is_err() && !iostat.is_null() {
1433 unsafe {
1434 *iostat = 1;
1435 }
1436 return;
1437 }
1438 let mut trailer = [0u8; 4];
1439 let _ = u.read_raw(&mut trailer);
1440 u.pending_read = Some((data, 0));
1441 }
1442
1443 /// End a list-directed READ statement. Drops any unread bytes left in
1444 /// the in-flight unformatted record buffer (the standard does not
1445 /// require the program to consume the entire record).
1446 #[no_mangle]
1447 pub extern "C" fn afs_list_read_end(
1448 unit: i32,
1449 _iostat: *mut i32,
1450 _iomsg: *mut u8,
1451 _iomsg_len: i64,
1452 ) {
1453 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1454 if let Some(u) = state.get_unit(unit) {
1455 u.pending_read = None;
1456 }
1457 }
1458
1459 /// Advance the file position past one record on a list-directed READ
1460 /// statement that has no input items: `read(unit, *)` (no items) is
1461 /// defined by F2018 §12.6.4.5 to position the unit at the next record.
1462 /// stdlib's `number_of_rows(s)` counts rows by repeating exactly that
1463 /// statement until a nonzero iostat — without this helper the loop is
1464 /// infinite because the unit never advances and iostat is never set.
1465 #[no_mangle]
1466 pub extern "C" fn afs_read_skip_record(unit: i32, iostat: *mut i32) {
1467 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1468 let Some(u) = state.get_unit(unit) else {
1469 if !iostat.is_null() {
1470 unsafe { *iostat = 1 };
1471 }
1472 return;
1473 };
1474 // Drain any pre-tokenized values from the previous list-directed
1475 // read so the next iteration genuinely consumes a new record.
1476 u.read_tokens.clear();
1477 match u.read_line() {
1478 Ok(s) if s.is_empty() => {
1479 if !iostat.is_null() {
1480 unsafe { *iostat = IOSTAT_END };
1481 }
1482 }
1483 Ok(_) => {
1484 if !iostat.is_null() {
1485 unsafe { *iostat = 0 };
1486 }
1487 }
1488 Err(_) => {
1489 if !iostat.is_null() {
1490 unsafe { *iostat = 1 };
1491 }
1492 }
1493 }
1494 }
1495
1496 #[no_mangle]
1497 pub extern "C" fn afs_read_string(unit: i32, dest: *mut u8, dest_len: i64, iostat: *mut i32) {
1498 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1499 let Some(u) = state.get_unit(unit) else {
1500 if !iostat.is_null() {
1501 unsafe {
1502 *iostat = 1;
1503 }
1504 }
1505 return;
1506 };
1507
1508 if dest_len < 0 {
1509 if !iostat.is_null() {
1510 unsafe {
1511 *iostat = 1;
1512 }
1513 }
1514 return;
1515 }
1516
1517 if let Some(bytes) = u.read_buffer_take(dest_len as usize) {
1518 crate::string::afs_assign_char_fixed(dest, dest_len, bytes.as_ptr(), dest_len);
1519 if !iostat.is_null() {
1520 unsafe {
1521 *iostat = 0;
1522 }
1523 }
1524 return;
1525 }
1526
1527 if u.form == Form::Unformatted && u.access == Access::Stream {
1528 let mut bytes = vec![b' '; dest_len as usize];
1529 match u.read_raw(&mut bytes) {
1530 Ok(0) => {
1531 crate::string::afs_assign_char_fixed(dest, dest_len, std::ptr::null(), 0);
1532 if !iostat.is_null() {
1533 unsafe {
1534 *iostat = IOSTAT_END;
1535 }
1536 }
1537 }
1538 Ok(n) => {
1539 crate::string::afs_assign_char_fixed(dest, dest_len, bytes.as_ptr(), n as i64);
1540 if !iostat.is_null() {
1541 unsafe {
1542 *iostat = 0;
1543 }
1544 }
1545 }
1546 Err(_) => {
1547 crate::string::afs_assign_char_fixed(dest, dest_len, std::ptr::null(), 0);
1548 if !iostat.is_null() {
1549 unsafe {
1550 *iostat = 1;
1551 }
1552 }
1553 }
1554 }
1555 return;
1556 }
1557
1558 match u.next_read_token() {
1559 Ok(Some(token)) => {
1560 crate::string::afs_assign_char_fixed(
1561 dest,
1562 dest_len,
1563 token.as_ptr(),
1564 token.len() as i64,
1565 );
1566 if !iostat.is_null() {
1567 unsafe {
1568 *iostat = 0;
1569 }
1570 }
1571 }
1572 Ok(None) => {
1573 crate::string::afs_assign_char_fixed(dest, dest_len, std::ptr::null(), 0);
1574 if !iostat.is_null() {
1575 unsafe {
1576 *iostat = IOSTAT_END;
1577 }
1578 }
1579 }
1580 Err(_) => {
1581 crate::string::afs_assign_char_fixed(dest, dest_len, std::ptr::null(), 0);
1582 if !iostat.is_null() {
1583 unsafe {
1584 *iostat = 1;
1585 }
1586 }
1587 }
1588 }
1589 }
1590
1591 // ---- Helpers ----
1592
1593 fn unsafe_str(ptr: *const u8, len: i64) -> String {
1594 if ptr.is_null() || len <= 0 {
1595 String::new()
1596 } else {
1597 let slice = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
1598 String::from_utf8_lossy(slice).into_owned()
1599 }
1600 }
1601
1602 // ---- Direct access helpers ----
1603
1604 impl Unit {
1605 /// Seek to a specific record for direct access.
1606 /// Record numbers are 1-based. Returns Ok(()) or Err on failure.
1607 fn seek_to_record(&mut self, rec: i64) -> io::Result<()> {
1608 if rec < 1 {
1609 return Err(io::Error::new(
1610 io::ErrorKind::InvalidInput,
1611 "direct access record number must be >= 1",
1612 ));
1613 }
1614 let recl = self.recl.ok_or_else(|| {
1615 io::Error::new(io::ErrorKind::InvalidInput, "direct access requires RECL")
1616 })?;
1617 let offset = (rec - 1)
1618 .checked_mul(recl)
1619 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "record offset overflow"))?;
1620 if offset < 0 {
1621 return Err(io::Error::new(
1622 io::ErrorKind::InvalidInput,
1623 "negative record offset",
1624 ));
1625 }
1626 match &mut self.stream {
1627 UnitStream::FileRaw(f) => {
1628 f.seek(SeekFrom::Start(offset as u64))?;
1629 Ok(())
1630 }
1631 _ => Err(io::Error::new(
1632 io::ErrorKind::InvalidInput,
1633 "unit not opened for direct access",
1634 )),
1635 }
1636 }
1637
1638 /// Write raw bytes at the current file position (for unformatted/stream I/O).
1639 fn write_raw(&mut self, data: &[u8]) -> io::Result<()> {
1640 match &mut self.stream {
1641 UnitStream::FileRaw(f) => f.write_all(data),
1642 UnitStream::FileWrite(w) => w.write_all(data),
1643 _ => Err(io::Error::new(
1644 io::ErrorKind::PermissionDenied,
1645 "unit not open for writing",
1646 )),
1647 }
1648 }
1649
1650 /// Read raw bytes at the current file position (for unformatted/stream I/O).
1651 fn read_raw(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1652 match &mut self.stream {
1653 UnitStream::FileRaw(f) => f.read(buf),
1654 UnitStream::FileRead(r) => r.read(buf),
1655 _ => Err(io::Error::new(
1656 io::ErrorKind::PermissionDenied,
1657 "unit not open for reading",
1658 )),
1659 }
1660 }
1661 }
1662
1663 /// Write a direct-access record (formatted string padded to recl).
1664 #[no_mangle]
1665 pub extern "C" fn afs_write_direct(
1666 unit: i32,
1667 rec: i64,
1668 data: *const u8,
1669 data_len: i64,
1670 iostat: *mut i32,
1671 ) {
1672 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1673 if let Some(u) = state.get_unit(unit) {
1674 if let Err(e) = u.seek_to_record(rec) {
1675 if !iostat.is_null() {
1676 unsafe {
1677 *iostat = 1;
1678 }
1679 } else {
1680 eprintln!("WRITE direct: {}", e);
1681 std::process::exit(1);
1682 }
1683 return;
1684 }
1685 let recl = u.recl.unwrap_or(0) as usize;
1686 let mut record = vec![b' '; recl]; // space-padded
1687 let copy_len = (data_len as usize).min(recl);
1688 if !data.is_null() && copy_len > 0 {
1689 unsafe {
1690 std::ptr::copy_nonoverlapping(data, record.as_mut_ptr(), copy_len);
1691 }
1692 }
1693 if let Err(e) = u.write_raw(&record) {
1694 if !iostat.is_null() {
1695 unsafe {
1696 *iostat = 1;
1697 }
1698 } else {
1699 eprintln!("WRITE direct: {}", e);
1700 std::process::exit(1);
1701 }
1702 return;
1703 }
1704 if !iostat.is_null() {
1705 unsafe {
1706 *iostat = 0;
1707 }
1708 }
1709 }
1710 }
1711
1712 /// Read a direct-access record.
1713 #[no_mangle]
1714 pub extern "C" fn afs_read_direct(
1715 unit: i32,
1716 rec: i64,
1717 data: *mut u8,
1718 data_len: i64,
1719 iostat: *mut i32,
1720 ) {
1721 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1722 if let Some(u) = state.get_unit(unit) {
1723 if let Err(e) = u.seek_to_record(rec) {
1724 if !iostat.is_null() {
1725 unsafe {
1726 *iostat = 1;
1727 }
1728 } else {
1729 eprintln!("READ direct: {}", e);
1730 std::process::exit(1);
1731 }
1732 return;
1733 }
1734 let recl = u.recl.unwrap_or(0) as usize;
1735 let mut record = vec![0u8; recl];
1736 match u.read_raw(&mut record) {
1737 Ok(n) => {
1738 let copy_len = n.min(data_len as usize);
1739 if !data.is_null() && copy_len > 0 {
1740 unsafe {
1741 std::ptr::copy_nonoverlapping(record.as_ptr(), data, copy_len);
1742 }
1743 }
1744 // Pad remainder with spaces.
1745 if copy_len < data_len as usize {
1746 unsafe {
1747 std::ptr::write_bytes(
1748 data.add(copy_len),
1749 b' ',
1750 data_len as usize - copy_len,
1751 );
1752 }
1753 }
1754 if !iostat.is_null() {
1755 unsafe {
1756 *iostat = 0;
1757 }
1758 }
1759 }
1760 Err(e) => {
1761 if !iostat.is_null() {
1762 unsafe {
1763 *iostat = 1;
1764 }
1765 } else {
1766 eprintln!("READ direct: {}", e);
1767 std::process::exit(1);
1768 }
1769 }
1770 }
1771 }
1772 }
1773
1774 // ---- Unformatted sequential I/O ----
1775
1776 /// Write an unformatted record with 4-byte length markers (gfortran-compatible).
1777 #[no_mangle]
1778 pub extern "C" fn afs_write_unformatted(
1779 unit: i32,
1780 data: *const u8,
1781 data_len: i64,
1782 iostat: *mut i32,
1783 ) {
1784 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1785 if let Some(u) = state.get_unit(unit) {
1786 if data_len < 0 || data_len > u32::MAX as i64 {
1787 if !iostat.is_null() {
1788 unsafe {
1789 *iostat = 1;
1790 }
1791 } else {
1792 eprintln!(
1793 "WRITE unformatted: record length {} exceeds 4GB limit",
1794 data_len
1795 );
1796 std::process::exit(1);
1797 }
1798 return;
1799 }
1800 let len_bytes = (data_len as u32).to_ne_bytes();
1801 // Write: [len][data][len]
1802 let r1 = u.write_raw(&len_bytes);
1803 let r2 = if !data.is_null() && data_len > 0 {
1804 let slice = unsafe { std::slice::from_raw_parts(data, data_len as usize) };
1805 u.write_raw(slice)
1806 } else {
1807 Ok(())
1808 };
1809 let r3 = u.write_raw(&len_bytes);
1810 if r1.is_err() || r2.is_err() || r3.is_err() {
1811 if !iostat.is_null() {
1812 unsafe {
1813 *iostat = 1;
1814 }
1815 }
1816 } else {
1817 if !iostat.is_null() {
1818 unsafe {
1819 *iostat = 0;
1820 }
1821 }
1822 }
1823 }
1824 }
1825
1826 /// Read an unformatted record with 4-byte length markers.
1827 #[no_mangle]
1828 pub extern "C" fn afs_read_unformatted(
1829 unit: i32,
1830 data: *mut u8,
1831 max_len: i64,
1832 actual_len: *mut i64,
1833 iostat: *mut i32,
1834 ) {
1835 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1836 if let Some(u) = state.get_unit(unit) {
1837 // Read leading length marker.
1838 let mut len_buf = [0u8; 4];
1839 match u.read_raw(&mut len_buf) {
1840 Ok(4) => {}
1841 Ok(0) => {
1842 if !iostat.is_null() {
1843 unsafe {
1844 *iostat = IOSTAT_END;
1845 }
1846 }
1847 return;
1848 }
1849 _ => {
1850 if !iostat.is_null() {
1851 unsafe {
1852 *iostat = 1;
1853 }
1854 }
1855 return;
1856 }
1857 }
1858 let record_len = u32::from_ne_bytes(len_buf) as usize;
1859 if !actual_len.is_null() {
1860 unsafe {
1861 *actual_len = record_len as i64;
1862 }
1863 }
1864
1865 // Read record data.
1866 let read_len = record_len.min(max_len as usize);
1867 if !data.is_null() && read_len > 0 {
1868 let slice = unsafe { std::slice::from_raw_parts_mut(data, read_len) };
1869 let _ = u.read_raw(slice);
1870 }
1871 // Skip remaining if record is longer than buffer.
1872 if record_len > max_len as usize {
1873 let skip = record_len - max_len as usize;
1874 let mut trash = vec![0u8; skip];
1875 let _ = u.read_raw(&mut trash);
1876 }
1877
1878 // Read trailing length marker (and discard).
1879 let _ = u.read_raw(&mut len_buf);
1880 if !iostat.is_null() {
1881 unsafe {
1882 *iostat = 0;
1883 }
1884 }
1885 }
1886 }
1887
1888 // ---- Stream access helpers ----
1889
1890 /// Write raw bytes at the current stream position.
1891 #[no_mangle]
1892 pub extern "C" fn afs_write_stream(unit: i32, data: *const u8, data_len: i64, iostat: *mut i32) {
1893 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1894 if let Some(u) = state.get_unit(unit) {
1895 if !data.is_null() && data_len > 0 {
1896 let slice = unsafe { std::slice::from_raw_parts(data, data_len as usize) };
1897 match u.write_raw(slice) {
1898 Ok(()) => {
1899 if !iostat.is_null() {
1900 unsafe {
1901 *iostat = 0;
1902 }
1903 }
1904 }
1905 Err(_) => {
1906 if !iostat.is_null() {
1907 unsafe {
1908 *iostat = 1;
1909 }
1910 }
1911 }
1912 }
1913 }
1914 }
1915 }
1916
1917 /// Seek to an absolute byte position in a stream unit.
1918 #[no_mangle]
1919 pub extern "C" fn afs_seek_stream(unit: i32, pos: i64, iostat: *mut i32) {
1920 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1921 if let Some(u) = state.get_unit(unit) {
1922 match &mut u.stream {
1923 UnitStream::FileRaw(f) => match f.seek(SeekFrom::Start(pos as u64)) {
1924 Ok(_) => {
1925 if !iostat.is_null() {
1926 unsafe {
1927 *iostat = 0;
1928 }
1929 }
1930 }
1931 Err(_) => {
1932 if !iostat.is_null() {
1933 unsafe {
1934 *iostat = 1;
1935 }
1936 }
1937 }
1938 },
1939 _ => {
1940 if !iostat.is_null() {
1941 unsafe {
1942 *iostat = 1;
1943 }
1944 }
1945 }
1946 }
1947 }
1948 }
1949
1950 // ---- NAMELIST I/O ----
1951
1952 /// A NAMELIST entry describing one variable in a namelist group.
1953 #[repr(C)]
1954 pub struct NamelistEntry {
1955 pub name: *const u8,
1956 pub name_len: i64,
1957 pub data: *mut u8,
1958 pub data_type: i32, // 0=int, 1=real, 2=string, 3=logical
1959 pub data_len: i64, // string length for type 2
1960 }
1961
1962 /// Write a NAMELIST group to a unit.
1963 /// Format: &GROUPNAME var=val, var=val, ... /
1964 #[no_mangle]
1965 pub extern "C" fn afs_write_namelist(
1966 unit: i32,
1967 group_name: *const u8,
1968 group_name_len: i64,
1969 entries: *const NamelistEntry,
1970 n_entries: i32,
1971 ) {
1972 let gname = unsafe_str(group_name, group_name_len);
1973 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
1974 if let Some(u) = state.get_unit(unit) {
1975 let _ = u.write_str(&format!(" &{}", gname.to_uppercase()));
1976
1977 if !entries.is_null() && n_entries > 0 {
1978 let slice = unsafe { std::slice::from_raw_parts(entries, n_entries as usize) };
1979 for (i, entry) in slice.iter().enumerate() {
1980 let name = unsafe_str(entry.name, entry.name_len);
1981 let sep = if i > 0 { "," } else { "" };
1982 let val_str = match entry.data_type {
1983 0 => {
1984 // integer
1985 let v = unsafe { *(entry.data as *const i32) };
1986 format!("{}", v)
1987 }
1988 1 => {
1989 // real
1990 let v = unsafe { *(entry.data as *const f64) };
1991 format!("{}", v)
1992 }
1993 2 => {
1994 // string
1995 let s = unsafe_str(entry.data, entry.data_len);
1996 format!("'{}'", s.trim_end())
1997 }
1998 3 => {
1999 // logical
2000 let v = unsafe { *(entry.data as *const i32) };
2001 (if v != 0 { ".TRUE." } else { ".FALSE." }).to_string()
2002 }
2003 _ => "???".to_string(),
2004 };
2005 let _ = u.write_str(&format!("{} {}={}", sep, name.to_uppercase(), val_str));
2006 }
2007 }
2008 let _ = u.write_str(" /\n");
2009 let _ = u.flush();
2010 }
2011 }
2012
2013 /// Read a NAMELIST group from a unit.
2014 /// Parses &GROUPNAME var=val, ... / format.
2015 #[no_mangle]
2016 pub extern "C" fn afs_read_namelist(
2017 unit: i32,
2018 group_name: *const u8,
2019 group_name_len: i64,
2020 entries: *const NamelistEntry,
2021 n_entries: i32,
2022 iostat: *mut i32,
2023 ) {
2024 let gname = unsafe_str(group_name, group_name_len).to_lowercase();
2025 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
2026 if let Some(u) = state.get_unit(unit) {
2027 // Read lines until we find &groupname.
2028 let mut all_lines = String::new();
2029 loop {
2030 match u.read_line() {
2031 Ok(line) => {
2032 let trimmed = line.trim().to_lowercase();
2033 if trimmed.starts_with('&') && trimmed[1..].starts_with(&gname) {
2034 all_lines.push_str(&line);
2035 // Keep reading until we find '/'.
2036 while !all_lines.contains('/') {
2037 match u.read_line() {
2038 Ok(cont) => all_lines.push_str(&cont),
2039 Err(_) => break,
2040 }
2041 }
2042 break;
2043 }
2044 }
2045 Err(_) => {
2046 if !iostat.is_null() {
2047 unsafe {
2048 *iostat = IOSTAT_END;
2049 }
2050 }
2051 return;
2052 }
2053 }
2054 }
2055
2056 // Parse assignments from the namelist text.
2057 if !entries.is_null() && n_entries > 0 {
2058 let entries_slice = unsafe { std::slice::from_raw_parts(entries, n_entries as usize) };
2059 // Extract the content between & and /.
2060 let content = if let Some(start) = all_lines.find(&gname) {
2061 let after_name = &all_lines[start + gname.len()..];
2062 if let Some(end) = after_name.find('/') {
2063 &after_name[..end]
2064 } else {
2065 after_name
2066 }
2067 } else {
2068 ""
2069 };
2070
2071 // Parse var=val pairs. Supports:
2072 // var=val — simple scalar assignment
2073 // var(index)=val — array element assignment (1-based)
2074 // var=n*val — repeat notation (set n consecutive elements)
2075 for pair in content.split(',') {
2076 let pair = pair.trim();
2077 if let Some(eq_pos) = pair.find('=') {
2078 let lhs = pair[..eq_pos].trim().to_lowercase();
2079 let val_str = pair[eq_pos + 1..].trim();
2080
2081 // Parse array index from "var(idx)" syntax.
2082 let (var_name, array_index) = if let Some(paren) = lhs.find('(') {
2083 let name = lhs[..paren].trim();
2084 let idx_str = lhs[paren + 1..].trim_end_matches(')').trim();
2085 let idx = idx_str.parse::<usize>().unwrap_or(1);
2086 (name.to_string(), Some(idx))
2087 } else {
2088 (lhs, None)
2089 };
2090
2091 // Parse repeat notation "n*val".
2092 let (repeat_count, actual_val) = if let Some(star) = val_str.find('*') {
2093 // Make sure * is preceded by digits (not part of a number like 1.5E*).
2094 let before = val_str[..star].trim();
2095 if let Ok(n) = before.parse::<usize>() {
2096 (n, val_str[star + 1..].trim())
2097 } else {
2098 (1, val_str)
2099 }
2100 } else {
2101 (1, val_str)
2102 };
2103
2104 // Find the matching entry.
2105 for entry in entries_slice {
2106 if entry.data.is_null() {
2107 continue;
2108 }
2109 let ename = unsafe_str(entry.name, entry.name_len).to_lowercase();
2110 if ename == var_name {
2111 namelist_assign_value(entry, actual_val, array_index, repeat_count);
2112 break;
2113 }
2114 }
2115 }
2116 }
2117 }
2118 if !iostat.is_null() {
2119 unsafe {
2120 *iostat = 0;
2121 }
2122 }
2123 }
2124 }
2125
2126 /// Assign a parsed NAMELIST value to an entry, handling array indexing and repeat.
2127 fn namelist_assign_value(
2128 entry: &NamelistEntry,
2129 val_str: &str,
2130 index: Option<usize>,
2131 repeat: usize,
2132 ) {
2133 // For array elements, compute byte offset from 1-based index.
2134 let elem_size = match entry.data_type {
2135 0 => 4, // integer (i32)
2136 1 => 8, // real (f64)
2137 3 => 4, // logical (i32)
2138 _ => 1, // string
2139 };
2140 let base_offset = index
2141 .map(|i| (i.saturating_sub(1)) * elem_size)
2142 .unwrap_or(0);
2143
2144 for r in 0..repeat {
2145 let offset = base_offset + r * elem_size;
2146 let ptr = unsafe { entry.data.add(offset) };
2147 match entry.data_type {
2148 0 => {
2149 // integer
2150 if let Ok(v) = val_str.parse::<i32>() {
2151 unsafe {
2152 *(ptr as *mut i32) = v;
2153 }
2154 }
2155 }
2156 1 => {
2157 // real
2158 let normalized = val_str.replace('d', "e").replace('D', "E");
2159 if let Ok(v) = normalized.parse::<f64>() {
2160 unsafe {
2161 *(ptr as *mut f64) = v;
2162 }
2163 }
2164 }
2165 2 => {
2166 // string (only first element for repeat, no array stride for strings)
2167 let s = val_str.trim_matches('\'').trim_matches('"');
2168 let bytes = s.as_bytes();
2169 let copy_len = bytes.len().min(entry.data_len as usize);
2170 if copy_len > 0 {
2171 unsafe {
2172 std::ptr::copy_nonoverlapping(bytes.as_ptr(), entry.data, copy_len);
2173 if copy_len < entry.data_len as usize {
2174 std::ptr::write_bytes(
2175 entry.data.add(copy_len),
2176 b' ',
2177 entry.data_len as usize - copy_len,
2178 );
2179 }
2180 }
2181 }
2182 return; // string repeat doesn't make sense
2183 }
2184 3 => {
2185 // logical
2186 let lower = val_str.to_lowercase();
2187 let v = lower.starts_with(".t") || lower.starts_with("t");
2188 unsafe {
2189 *(ptr as *mut i32) = v as i32;
2190 }
2191 }
2192 _ => {}
2193 }
2194 }
2195 }
2196
2197 // ---- Internal I/O (read/write to character variables) ----
2198
2199 /// Write a formatted integer to a character buffer (internal I/O).
2200 #[no_mangle]
2201 pub extern "C" fn afs_write_internal_int(
2202 buf: *mut u8,
2203 buf_len: i64,
2204 val: i32,
2205 pos: *mut i64, // current write position, updated after write
2206 ) {
2207 if buf.is_null() || buf_len <= 0 {
2208 return;
2209 }
2210 let s = format!(" {}", val);
2211 let start = if !pos.is_null() {
2212 (unsafe { *pos }) as usize
2213 } else {
2214 0
2215 };
2216 write_to_buffer(buf, buf_len as usize, start, s.as_bytes(), pos);
2217 }
2218
2219 /// Write a formatted i64 to a character buffer (internal I/O).
2220 #[no_mangle]
2221 pub extern "C" fn afs_write_internal_int64(buf: *mut u8, buf_len: i64, val: i64, pos: *mut i64) {
2222 if buf.is_null() || buf_len <= 0 {
2223 return;
2224 }
2225 let s = format!(" {}", val);
2226 let start = if !pos.is_null() {
2227 (unsafe { *pos }) as usize
2228 } else {
2229 0
2230 };
2231 write_to_buffer(buf, buf_len as usize, start, s.as_bytes(), pos);
2232 }
2233
2234 /// Write a formatted integer(16) to a character buffer (internal I/O).
2235 #[no_mangle]
2236 pub extern "C" fn afs_write_internal_int128(buf: *mut u8, buf_len: i64, val: i128, pos: *mut i64) {
2237 if buf.is_null() || buf_len <= 0 {
2238 return;
2239 }
2240 let s = format!(" {}", val);
2241 let start = if !pos.is_null() {
2242 (unsafe { *pos }) as usize
2243 } else {
2244 0
2245 };
2246 write_to_buffer(buf, buf_len as usize, start, s.as_bytes(), pos);
2247 }
2248
2249 /// Write a formatted real to a character buffer (internal I/O).
2250 #[no_mangle]
2251 pub extern "C" fn afs_write_internal_real64(buf: *mut u8, buf_len: i64, val: f64, pos: *mut i64) {
2252 if buf.is_null() || buf_len <= 0 {
2253 return;
2254 }
2255 let s = format!(" {}", val);
2256 let start = if !pos.is_null() {
2257 (unsafe { *pos }) as usize
2258 } else {
2259 0
2260 };
2261 write_to_buffer(buf, buf_len as usize, start, s.as_bytes(), pos);
2262 }
2263
2264 /// Write a formatted string to a character buffer (internal I/O).
2265 #[no_mangle]
2266 pub extern "C" fn afs_write_internal_string(
2267 buf: *mut u8,
2268 buf_len: i64,
2269 src: *const u8,
2270 src_len: i64,
2271 pos: *mut i64,
2272 ) {
2273 if buf.is_null() || buf_len <= 0 {
2274 return;
2275 }
2276 let start = if !pos.is_null() {
2277 (unsafe { *pos }) as usize
2278 } else {
2279 0
2280 };
2281 let mut data = vec![b' '];
2282 if !src.is_null() && src_len > 0 {
2283 let slice = unsafe { std::slice::from_raw_parts(src, src_len as usize) };
2284 data.extend_from_slice(slice);
2285 }
2286 write_to_buffer(buf, buf_len as usize, start, &data, pos);
2287 }
2288
2289 fn next_internal_token(buf: *const u8, buf_len: i64, pos: *mut i64) -> Option<String> {
2290 if buf.is_null() || buf_len <= 0 {
2291 return None;
2292 }
2293
2294 let slice = unsafe { std::slice::from_raw_parts(buf, buf_len as usize) };
2295 let mut idx = if !pos.is_null() {
2296 unsafe { (*pos).clamp(0, buf_len) as usize }
2297 } else {
2298 0
2299 };
2300
2301 while idx < slice.len() && (slice[idx].is_ascii_whitespace() || slice[idx] == b',') {
2302 idx += 1;
2303 }
2304
2305 if idx >= slice.len() {
2306 if !pos.is_null() {
2307 unsafe {
2308 *pos = idx as i64;
2309 }
2310 }
2311 return None;
2312 }
2313
2314 let start = idx;
2315 while idx < slice.len() && !slice[idx].is_ascii_whitespace() && slice[idx] != b',' {
2316 idx += 1;
2317 }
2318
2319 if !pos.is_null() {
2320 unsafe {
2321 *pos = idx as i64;
2322 }
2323 }
2324
2325 Some(String::from_utf8_lossy(&slice[start..idx]).into_owned())
2326 }
2327
2328 /// Read an integer from a character buffer (internal I/O).
2329 #[no_mangle]
2330 pub extern "C" fn afs_read_internal_int(
2331 buf: *const u8,
2332 buf_len: i64,
2333 pos: *mut i64,
2334 val: *mut i32,
2335 iostat: *mut i32,
2336 ) {
2337 if let Some(token) = next_internal_token(buf, buf_len, pos) {
2338 match token.replace(',', "").parse::<i32>() {
2339 Ok(v) => {
2340 if !val.is_null() {
2341 unsafe {
2342 *val = v;
2343 }
2344 }
2345 if !iostat.is_null() {
2346 unsafe {
2347 *iostat = 0;
2348 }
2349 }
2350 }
2351 Err(_) => {
2352 if !iostat.is_null() {
2353 unsafe {
2354 *iostat = 1;
2355 }
2356 }
2357 }
2358 }
2359 } else {
2360 if !iostat.is_null() {
2361 unsafe {
2362 *iostat = -1;
2363 }
2364 }
2365 }
2366 }
2367
2368 /// Read an i64 from a character buffer (internal I/O).
2369 #[no_mangle]
2370 pub extern "C" fn afs_read_internal_int64(
2371 buf: *const u8,
2372 buf_len: i64,
2373 pos: *mut i64,
2374 val: *mut i64,
2375 iostat: *mut i32,
2376 ) {
2377 if let Some(token) = next_internal_token(buf, buf_len, pos) {
2378 match token.replace(',', "").parse::<i64>() {
2379 Ok(v) => {
2380 if !val.is_null() {
2381 unsafe {
2382 *val = v;
2383 }
2384 }
2385 if !iostat.is_null() {
2386 unsafe {
2387 *iostat = 0;
2388 }
2389 }
2390 }
2391 Err(_) => {
2392 if !iostat.is_null() {
2393 unsafe {
2394 *iostat = 1;
2395 }
2396 }
2397 }
2398 }
2399 } else {
2400 if !iostat.is_null() {
2401 unsafe {
2402 *iostat = -1;
2403 }
2404 }
2405 }
2406 }
2407
2408 /// Read an integer(16) from a character buffer (internal I/O).
2409 #[no_mangle]
2410 pub extern "C" fn afs_read_internal_int128(
2411 buf: *const u8,
2412 buf_len: i64,
2413 pos: *mut i64,
2414 val: *mut i128,
2415 iostat: *mut i32,
2416 ) {
2417 if let Some(token) = next_internal_token(buf, buf_len, pos) {
2418 match token.replace(',', "").parse::<i128>() {
2419 Ok(v) => {
2420 write_i128_ptr(val, v);
2421 if !iostat.is_null() {
2422 unsafe {
2423 *iostat = 0;
2424 }
2425 }
2426 }
2427 Err(_) => {
2428 if !iostat.is_null() {
2429 unsafe {
2430 *iostat = 1;
2431 }
2432 }
2433 }
2434 }
2435 } else {
2436 if !iostat.is_null() {
2437 unsafe {
2438 *iostat = -1;
2439 }
2440 }
2441 }
2442 }
2443
2444 /// Read a real from a character buffer (internal I/O).
2445 #[no_mangle]
2446 pub extern "C" fn afs_read_internal_real(
2447 buf: *const u8,
2448 buf_len: i64,
2449 pos: *mut i64,
2450 val: *mut f64,
2451 iostat: *mut i32,
2452 ) {
2453 if let Some(token) = next_internal_token(buf, buf_len, pos) {
2454 let normalized = token.replace('d', "e").replace('D', "E").replace(',', "");
2455 match normalized.parse::<f64>() {
2456 Ok(v) => {
2457 if !val.is_null() {
2458 unsafe {
2459 *val = v;
2460 }
2461 }
2462 if !iostat.is_null() {
2463 unsafe {
2464 *iostat = 0;
2465 }
2466 }
2467 }
2468 Err(_) => {
2469 if !iostat.is_null() {
2470 unsafe {
2471 *iostat = 1;
2472 }
2473 }
2474 }
2475 }
2476 } else {
2477 if !iostat.is_null() {
2478 unsafe {
2479 *iostat = -1;
2480 }
2481 }
2482 }
2483 }
2484
2485 /// Helper: write bytes into a buffer at a given position, space-pad remainder.
2486 fn write_to_buffer(buf: *mut u8, buf_len: usize, start: usize, data: &[u8], pos: *mut i64) {
2487 let copy_len = data.len().min(buf_len.saturating_sub(start));
2488 if copy_len > 0 {
2489 unsafe {
2490 std::ptr::copy_nonoverlapping(data.as_ptr(), buf.add(start), copy_len);
2491 }
2492 }
2493 // Space-pad remaining buffer.
2494 let end = start + copy_len;
2495 if end < buf_len {
2496 unsafe {
2497 std::ptr::write_bytes(buf.add(end), b' ', buf_len - end);
2498 }
2499 }
2500 if !pos.is_null() {
2501 unsafe {
2502 *pos = end as i64;
2503 }
2504 }
2505 }
2506
2507 // ---- BACKSPACE / ENDFILE ----
2508
2509 /// Backspace one record on a sequential unit.
2510 /// For formatted: seek backwards past the previous newline.
2511 #[no_mangle]
2512 pub extern "C" fn afs_backspace(unit: i32, iostat: *mut i32) {
2513 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
2514 if let Some(u) = state.get_unit(unit) {
2515 match &mut u.stream {
2516 UnitStream::FileRaw(f) => {
2517 // Simple approach: seek backwards byte-by-byte to find newline.
2518 let pos = f.stream_position().unwrap_or(0);
2519 if pos <= 1 {
2520 let _ = f.seek(SeekFrom::Start(0));
2521 if !iostat.is_null() {
2522 unsafe {
2523 *iostat = 0;
2524 }
2525 }
2526 // Clear stale read tokens.
2527 u.read_tokens.clear();
2528 return;
2529 }
2530 // Skip the current newline at pos-1.
2531 let mut search_pos = pos - 2;
2532 loop {
2533 f.seek(SeekFrom::Start(search_pos)).ok();
2534 let mut byte = [0u8; 1];
2535 if f.read(&mut byte).unwrap_or(0) == 0 {
2536 break;
2537 }
2538 if byte[0] == b'\n' {
2539 f.seek(SeekFrom::Start(search_pos + 1)).ok();
2540 break;
2541 }
2542 if search_pos == 0 {
2543 f.seek(SeekFrom::Start(0)).ok();
2544 break;
2545 }
2546 search_pos -= 1;
2547 }
2548 // Clear stale read tokens after repositioning.
2549 u.read_tokens.clear();
2550 if !iostat.is_null() {
2551 unsafe {
2552 *iostat = 0;
2553 }
2554 }
2555 }
2556 _ => {
2557 // Sequential buffered files: backspace is not well-supported.
2558 if !iostat.is_null() {
2559 unsafe {
2560 *iostat = 0;
2561 }
2562 }
2563 }
2564 }
2565 }
2566 }
2567
2568 /// Write an end-of-file marker and truncate.
2569 #[no_mangle]
2570 pub extern "C" fn afs_endfile(unit: i32, iostat: *mut i32) {
2571 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
2572 if let Some(u) = state.get_unit(unit) {
2573 let _ = u.flush();
2574 if let UnitStream::FileRaw(f) = &mut u.stream {
2575 let pos = f.stream_position().unwrap_or(0);
2576 let _ = f.set_len(pos); // truncate at current position
2577 }
2578 if !iostat.is_null() {
2579 unsafe {
2580 *iostat = 0;
2581 }
2582 }
2583 }
2584 }
2585
2586 // ---- IOSTAT constants (iso_fortran_env) ----
2587
2588 /// IOSTAT_END: end-of-file encountered during input.
2589 pub const IOSTAT_END: i32 = -1;
2590 /// IOSTAT_EOR: end-of-record encountered during non-advancing input.
2591 pub const IOSTAT_EOR: i32 = -2;
2592
2593 // ---- INQUIRE ----
2594
2595 /// Write a Fortran-style string result into a caller-provided buffer.
2596 /// Pads with spaces to buf_len (Fortran CHARACTER semantics).
2597 fn write_inquire_string(buf: *mut u8, buf_len: i64, value: &str) {
2598 if buf.is_null() || buf_len <= 0 {
2599 return;
2600 }
2601 let n = buf_len as usize;
2602 let val_bytes = value.as_bytes();
2603 let copy_len = val_bytes.len().min(n);
2604 unsafe {
2605 std::ptr::copy_nonoverlapping(val_bytes.as_ptr(), buf, copy_len);
2606 // Pad remainder with spaces.
2607 if copy_len < n {
2608 std::ptr::write_bytes(buf.add(copy_len), b' ', n - copy_len);
2609 }
2610 }
2611 }
2612
2613 /// INQUIRE by file: check if a file exists, report its properties.
2614 #[no_mangle]
2615 pub extern "C" fn afs_inquire_file(
2616 filename: *const u8,
2617 filename_len: i64,
2618 exist: *mut i32,
2619 opened: *mut i32,
2620 iostat: *mut i32,
2621 // Extended specifiers — pass null for any not needed.
2622 name_buf: *mut u8,
2623 name_buf_len: i64,
2624 access_buf: *mut u8,
2625 access_buf_len: i64,
2626 form_buf: *mut u8,
2627 form_buf_len: i64,
2628 action_buf: *mut u8,
2629 action_buf_len: i64,
2630 recl_out: *mut i64,
2631 size_out: *mut i64,
2632 read_buf: *mut u8,
2633 read_buf_len: i64,
2634 write_buf: *mut u8,
2635 write_buf_len: i64,
2636 readwrite_buf: *mut u8,
2637 readwrite_buf_len: i64,
2638 ) {
2639 let fname = unsafe_str(filename, filename_len);
2640
2641 let file_exists = std::path::Path::new(&fname).exists();
2642 if !exist.is_null() {
2643 unsafe {
2644 *exist = file_exists as i32;
2645 }
2646 }
2647
2648 // Find unit connected to this file (if any).
2649 let state = io_state().lock().unwrap_or_else(|e| e.into_inner());
2650 let connected_unit = state.units.values().find(|u| u.filename == fname);
2651
2652 if !opened.is_null() {
2653 unsafe {
2654 *opened = connected_unit.is_some() as i32;
2655 }
2656 }
2657
2658 write_inquire_string(name_buf, name_buf_len, &fname);
2659
2660 if let Some(u) = connected_unit {
2661 write_unit_properties(
2662 u,
2663 access_buf,
2664 access_buf_len,
2665 form_buf,
2666 form_buf_len,
2667 action_buf,
2668 action_buf_len,
2669 recl_out,
2670 );
2671 write_action_capabilities(
2672 Some(u.action),
2673 read_buf,
2674 read_buf_len,
2675 write_buf,
2676 write_buf_len,
2677 readwrite_buf,
2678 readwrite_buf_len,
2679 );
2680 } else {
2681 write_inquire_string(access_buf, access_buf_len, "UNDEFINED");
2682 write_inquire_string(form_buf, form_buf_len, "UNDEFINED");
2683 write_inquire_string(action_buf, action_buf_len, "UNDEFINED");
2684 write_action_capabilities(
2685 None,
2686 read_buf,
2687 read_buf_len,
2688 write_buf,
2689 write_buf_len,
2690 readwrite_buf,
2691 readwrite_buf_len,
2692 );
2693 }
2694
2695 // File size via metadata.
2696 if !size_out.is_null() {
2697 let sz = std::fs::metadata(&fname)
2698 .map(|m| m.len() as i64)
2699 .unwrap_or(-1);
2700 unsafe {
2701 *size_out = sz;
2702 }
2703 }
2704
2705 if !iostat.is_null() {
2706 unsafe {
2707 *iostat = 0;
2708 }
2709 }
2710 }
2711
2712 /// INQUIRE by unit: check if a unit is connected, report its properties.
2713 #[no_mangle]
2714 pub extern "C" fn afs_inquire_unit(
2715 unit: i32,
2716 exist: *mut i32,
2717 opened: *mut i32,
2718 iostat: *mut i32,
2719 // Extended specifiers.
2720 name_buf: *mut u8,
2721 name_buf_len: i64,
2722 access_buf: *mut u8,
2723 access_buf_len: i64,
2724 form_buf: *mut u8,
2725 form_buf_len: i64,
2726 action_buf: *mut u8,
2727 action_buf_len: i64,
2728 recl_out: *mut i64,
2729 size_out: *mut i64,
2730 read_buf: *mut u8,
2731 read_buf_len: i64,
2732 write_buf: *mut u8,
2733 write_buf_len: i64,
2734 readwrite_buf: *mut u8,
2735 readwrite_buf_len: i64,
2736 ) {
2737 let state = io_state().lock().unwrap_or_else(|e| e.into_inner());
2738 let unit_entry = state.units.get(&unit);
2739
2740 if !exist.is_null() {
2741 unsafe {
2742 *exist = unit_entry.is_some() as i32;
2743 }
2744 }
2745 if !opened.is_null() {
2746 unsafe {
2747 *opened = unit_entry.is_some() as i32;
2748 }
2749 }
2750
2751 if let Some(u) = unit_entry {
2752 write_inquire_string(name_buf, name_buf_len, &u.filename);
2753 write_unit_properties(
2754 u,
2755 access_buf,
2756 access_buf_len,
2757 form_buf,
2758 form_buf_len,
2759 action_buf,
2760 action_buf_len,
2761 recl_out,
2762 );
2763 write_action_capabilities(
2764 Some(u.action),
2765 read_buf,
2766 read_buf_len,
2767 write_buf,
2768 write_buf_len,
2769 readwrite_buf,
2770 readwrite_buf_len,
2771 );
2772
2773 if !size_out.is_null() {
2774 let sz = if !u.filename.is_empty() {
2775 std::fs::metadata(&u.filename)
2776 .map(|m| m.len() as i64)
2777 .unwrap_or(-1)
2778 } else {
2779 -1
2780 };
2781 unsafe {
2782 *size_out = sz;
2783 }
2784 }
2785 } else {
2786 write_inquire_string(name_buf, name_buf_len, "");
2787 write_inquire_string(access_buf, access_buf_len, "UNDEFINED");
2788 write_inquire_string(form_buf, form_buf_len, "UNDEFINED");
2789 write_inquire_string(action_buf, action_buf_len, "UNDEFINED");
2790 write_action_capabilities(
2791 None,
2792 read_buf,
2793 read_buf_len,
2794 write_buf,
2795 write_buf_len,
2796 readwrite_buf,
2797 readwrite_buf_len,
2798 );
2799 if !size_out.is_null() {
2800 unsafe {
2801 *size_out = -1;
2802 }
2803 }
2804 }
2805
2806 if !iostat.is_null() {
2807 unsafe {
2808 *iostat = 0;
2809 }
2810 }
2811 }
2812
2813 /// Fill READ=, WRITE=, READWRITE= INQUIRE specifiers based on a unit's
2814 /// declared `Action`. Per F2018 §12.10.2, READ returns YES if the unit
2815 /// can be read, NO otherwise, and similarly for WRITE. Disconnected
2816 /// units (`action = None`) report UNKNOWN for all three.
2817 fn write_action_capabilities(
2818 action: Option<Action>,
2819 read_buf: *mut u8,
2820 read_buf_len: i64,
2821 write_buf: *mut u8,
2822 write_buf_len: i64,
2823 readwrite_buf: *mut u8,
2824 readwrite_buf_len: i64,
2825 ) {
2826 let (read_cap, write_cap, rw_cap) = match action {
2827 Some(Action::Read) => ("YES", "NO", "NO"),
2828 Some(Action::Write) => ("NO", "YES", "NO"),
2829 Some(Action::ReadWrite) => ("YES", "YES", "YES"),
2830 None => ("UNKNOWN", "UNKNOWN", "UNKNOWN"),
2831 };
2832 write_inquire_string(read_buf, read_buf_len, read_cap);
2833 write_inquire_string(write_buf, write_buf_len, write_cap);
2834 write_inquire_string(readwrite_buf, readwrite_buf_len, rw_cap);
2835 }
2836
2837 /// Write ACCESS, FORM, ACTION, RECL for a connected unit.
2838 fn write_unit_properties(
2839 u: &Unit,
2840 access_buf: *mut u8,
2841 access_buf_len: i64,
2842 form_buf: *mut u8,
2843 form_buf_len: i64,
2844 action_buf: *mut u8,
2845 action_buf_len: i64,
2846 recl_out: *mut i64,
2847 ) {
2848 let access_str = match u.access {
2849 Access::Sequential => "SEQUENTIAL",
2850 Access::Direct => "DIRECT",
2851 Access::Stream => "STREAM",
2852 };
2853 write_inquire_string(access_buf, access_buf_len, access_str);
2854
2855 let form_str = match u.form {
2856 Form::Formatted => "FORMATTED",
2857 Form::Unformatted => "UNFORMATTED",
2858 };
2859 write_inquire_string(form_buf, form_buf_len, form_str);
2860
2861 let action_str = match u.action {
2862 Action::Read => "READ",
2863 Action::Write => "WRITE",
2864 Action::ReadWrite => "READWRITE",
2865 };
2866 write_inquire_string(action_buf, action_buf_len, action_str);
2867
2868 if !recl_out.is_null() {
2869 unsafe {
2870 *recl_out = u.recl.unwrap_or(-1);
2871 }
2872 }
2873 }
2874
2875 // ---- FLUSH ----
2876
2877 /// Flush a unit's output buffer.
2878 #[no_mangle]
2879 pub extern "C" fn afs_flush(unit: i32, iostat: *mut i32) {
2880 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
2881 if let Some(u) = state.get_unit(unit) {
2882 match u.flush() {
2883 Ok(()) => {
2884 if !iostat.is_null() {
2885 unsafe {
2886 *iostat = 0;
2887 }
2888 }
2889 }
2890 Err(e) => {
2891 if !iostat.is_null() {
2892 unsafe {
2893 *iostat = e.raw_os_error().unwrap_or(1);
2894 }
2895 }
2896 }
2897 }
2898 }
2899 }
2900
2901 // ---- REWIND / BACKSPACE / ENDFILE ----
2902
2903 /// Rewind a unit to the beginning.
2904 #[no_mangle]
2905 pub extern "C" fn afs_rewind(unit: i32, iostat: *mut i32) {
2906 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
2907 if let Some(u) = state.get_unit(unit) {
2908 match &mut u.stream {
2909 UnitStream::FileRead(r) => {
2910 let _ = r.seek(SeekFrom::Start(0));
2911 }
2912 UnitStream::FileWrite(w) => {
2913 let _ = w.flush();
2914 let _ = w.seek(SeekFrom::Start(0));
2915 }
2916 UnitStream::FileRaw(f) => {
2917 let _ = f.seek(SeekFrom::Start(0));
2918 }
2919 _ => {}
2920 }
2921 // Clear stale read tokens so subsequent reads come from file start.
2922 u.read_tokens.clear();
2923 if !iostat.is_null() {
2924 unsafe {
2925 *iostat = 0;
2926 }
2927 }
2928 }
2929 }
2930
2931 // ---- Program lifecycle integration ----
2932
2933 /// Initialize the I/O subsystem. Called from afs_program_init.
2934 #[no_mangle]
2935 pub extern "C" fn afs_io_init() {
2936 // Force initialization of the global state.
2937 drop(io_state().lock());
2938 }
2939
2940 /// Finalize the I/O subsystem. Flush and close all open units.
2941 #[no_mangle]
2942 pub extern "C" fn afs_io_finalize() {
2943 // Use try_lock instead of lock: process::exit invokes libc atexit handlers,
2944 // and any I/O routine that exited while holding io_state would deadlock here.
2945 // If the caller is already holding the lock during exit, their drop has already
2946 // unwound or process::exit released their mutex first; in the rare case where
2947 // the lock is genuinely contested, skip flush rather than hang the program.
2948 if let Ok(mut state) = io_state().try_lock() {
2949 for (_, unit) in state.units.iter_mut() {
2950 let _ = unit.flush();
2951 }
2952 // Delete any STATUS='SCRATCH' backing files left open at exit.
2953 let scratch_paths: Vec<String> = state
2954 .units
2955 .values()
2956 .filter(|u| u.scratch && !u.filename.is_empty())
2957 .map(|u| u.filename.clone())
2958 .collect();
2959 for path in scratch_paths {
2960 let _ = std::fs::remove_file(&path);
2961 }
2962 }
2963 }
2964
2965 // ---- Formatted I/O (push-based API) ----
2966 //
2967 // The format engine (format.rs) parses format strings and applies descriptors
2968 // to values. This API lets compiled code push values one at a time, then flush
2969 // the formatted output in one call.
2970 //
2971 // Usage from codegen:
2972 // afs_fmt_begin(unit, fmt_str, fmt_len)
2973 // afs_fmt_push_int(val) / afs_fmt_push_int128(&val) / afs_fmt_push_real(val) / ...
2974 // afs_fmt_end()
2975
2976 use crate::format::{parse_format, FormatDesc, FormatEngine, IoValue};
2977 use std::cell::RefCell;
2978
2979 enum FmtSink {
2980 Unit(i32),
2981 Internal { buf: *mut u8, buf_len: usize },
2982 }
2983
2984 /// Thread-local state for the current formatted I/O operation.
2985 struct FmtContext {
2986 sink: FmtSink,
2987 format_str: String,
2988 values: Vec<IoValue>,
2989 iostat: *mut i32,
2990 iomsg: *mut u8,
2991 iomsg_len: i64,
2992 }
2993
2994 // SAFETY: FmtContext only lives inside a thread-local; the raw
2995 // pointers are written by the same thread that begins the I/O.
2996 unsafe impl Send for FmtContext {}
2997
2998 thread_local! {
2999 static FMT_CTX: RefCell<Option<FmtContext>> = const { RefCell::new(None) };
3000 }
3001
3002 /// Begin a formatted write operation. Parses the format string and prepares
3003 /// to accumulate values.
3004 #[no_mangle]
3005 pub extern "C" fn afs_fmt_begin(unit: i32, fmt_str: *const u8, fmt_len: i64) {
3006 afs_fmt_begin_ex(
3007 unit,
3008 fmt_str,
3009 fmt_len,
3010 std::ptr::null_mut(),
3011 std::ptr::null_mut(),
3012 0,
3013 );
3014 }
3015
3016 /// Extended formatted-write begin: accepts iostat and iomsg pointers
3017 /// captured from the WRITE-statement specs. Either pointer may be null.
3018 #[no_mangle]
3019 pub extern "C" fn afs_fmt_begin_ex(
3020 unit: i32,
3021 fmt_str: *const u8,
3022 fmt_len: i64,
3023 iostat: *mut i32,
3024 iomsg: *mut u8,
3025 iomsg_len: i64,
3026 ) {
3027 let fmt = unsafe_str(fmt_str, fmt_len);
3028 FMT_CTX.with(|ctx| {
3029 *ctx.borrow_mut() = Some(FmtContext {
3030 sink: FmtSink::Unit(unit),
3031 format_str: fmt,
3032 values: Vec::new(),
3033 iostat,
3034 iomsg,
3035 iomsg_len,
3036 });
3037 });
3038 }
3039
3040 /// Begin a formatted internal write operation targeting a character buffer.
3041 #[no_mangle]
3042 pub extern "C" fn afs_fmt_begin_internal(
3043 buf: *mut u8,
3044 buf_len: i64,
3045 fmt_str: *const u8,
3046 fmt_len: i64,
3047 ) {
3048 afs_fmt_begin_internal_ex(
3049 buf,
3050 buf_len,
3051 fmt_str,
3052 fmt_len,
3053 std::ptr::null_mut(),
3054 std::ptr::null_mut(),
3055 0,
3056 );
3057 }
3058
3059 /// Extended internal-write begin with iostat/iomsg plumbing.
3060 #[no_mangle]
3061 pub extern "C" fn afs_fmt_begin_internal_ex(
3062 buf: *mut u8,
3063 buf_len: i64,
3064 fmt_str: *const u8,
3065 fmt_len: i64,
3066 iostat: *mut i32,
3067 iomsg: *mut u8,
3068 iomsg_len: i64,
3069 ) {
3070 let fmt = unsafe_str(fmt_str, fmt_len);
3071 FMT_CTX.with(|ctx| {
3072 *ctx.borrow_mut() = Some(FmtContext {
3073 sink: FmtSink::Internal {
3074 buf,
3075 buf_len: buf_len.max(0) as usize,
3076 },
3077 format_str: fmt,
3078 values: Vec::new(),
3079 iostat,
3080 iomsg,
3081 iomsg_len,
3082 });
3083 });
3084 }
3085
3086 /// Push an integer value for formatted output.
3087 #[no_mangle]
3088 pub extern "C" fn afs_fmt_push_int(val: i64) {
3089 FMT_CTX.with(|ctx| {
3090 if let Some(ref mut c) = *ctx.borrow_mut() {
3091 c.values.push(IoValue::Integer(val as i128));
3092 }
3093 });
3094 }
3095
3096 /// Push an integer(16) value for formatted output.
3097 #[no_mangle]
3098 pub extern "C" fn afs_fmt_push_int128(val: *const i128) {
3099 FMT_CTX.with(|ctx| {
3100 if let Some(ref mut c) = *ctx.borrow_mut() {
3101 if let Some(wide) = read_i128_ptr(val) {
3102 c.values.push(IoValue::Integer(wide));
3103 }
3104 }
3105 });
3106 }
3107
3108 /// Push a real (f64) value for formatted output.
3109 #[no_mangle]
3110 pub extern "C" fn afs_fmt_push_real(val: f64) {
3111 FMT_CTX.with(|ctx| {
3112 if let Some(ref mut c) = *ctx.borrow_mut() {
3113 c.values.push(IoValue::Real(val));
3114 }
3115 });
3116 }
3117
3118 /// Push a logical value for formatted output.
3119 #[no_mangle]
3120 pub extern "C" fn afs_fmt_push_logical(val: i32) {
3121 FMT_CTX.with(|ctx| {
3122 if let Some(ref mut c) = *ctx.borrow_mut() {
3123 c.values.push(IoValue::Logical(val != 0));
3124 }
3125 });
3126 }
3127
3128 /// Push a character string value for formatted output.
3129 #[no_mangle]
3130 pub extern "C" fn afs_fmt_push_string(ptr: *const u8, len: i64) {
3131 let bytes = if !ptr.is_null() && len > 0 {
3132 unsafe { std::slice::from_raw_parts(ptr, len as usize) }.to_vec()
3133 } else {
3134 Vec::new()
3135 };
3136 FMT_CTX.with(|ctx| {
3137 if let Some(ref mut c) = *ctx.borrow_mut() {
3138 c.values.push(IoValue::Character(bytes));
3139 }
3140 });
3141 }
3142
3143 /// End the formatted write: apply the format engine and write the result.
3144 /// If advance is true (nonzero), appends a newline. If false (zero), no newline.
3145 #[no_mangle]
3146 pub extern "C" fn afs_fmt_end(advance: i32) {
3147 FMT_CTX.with(|ctx| {
3148 let context = ctx.borrow_mut().take();
3149 if let Some(c) = context {
3150 let descriptors = parse_format(&c.format_str);
3151 let mut engine = FormatEngine::new(descriptors);
3152 let output = engine.format_values(&c.values);
3153
3154 // Track success across the sink branches. List-directed and
3155 // scalar formatted writes both leave `iostat` untouched on
3156 // older builds; stdlib's savetxt loops `if (ios/=0) error_stop`
3157 // on the post-write value, so a write that silently leaves the
3158 // pre-call sentinel in place trips the error-stop branch every
3159 // iteration. Set `*iostat = 0` on success and stash an empty
3160 // iomsg so callers see a clean state.
3161 let mut io_status: i32 = 0;
3162 let mut io_msg: Option<&'static str> = None;
3163
3164 match c.sink {
3165 FmtSink::Unit(unit) => {
3166 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
3167 if let Some(u) = state.get_unit(unit) {
3168 if u.write_str(&output).is_err() {
3169 io_status = 1;
3170 io_msg = Some("write failed");
3171 }
3172 if io_status == 0 && advance != 0 && u.write_str("\n").is_err() {
3173 io_status = 1;
3174 io_msg = Some("write failed");
3175 }
3176 } else {
3177 io_status = 1;
3178 io_msg = Some("unit not connected");
3179 }
3180 }
3181 FmtSink::Internal { buf, buf_len } => {
3182 write_to_buffer(buf, buf_len, 0, output.as_bytes(), std::ptr::null_mut());
3183 }
3184 }
3185
3186 if !c.iostat.is_null() {
3187 unsafe { *c.iostat = io_status };
3188 }
3189 if !c.iomsg.is_null() && c.iomsg_len > 0 {
3190 let msg = io_msg.unwrap_or("");
3191 let cap = c.iomsg_len as usize;
3192 let bytes = msg.as_bytes();
3193 let copy = bytes.len().min(cap);
3194 unsafe {
3195 std::ptr::copy_nonoverlapping(bytes.as_ptr(), c.iomsg, copy);
3196 if copy < cap {
3197 // Pad remainder with spaces per Fortran character semantics.
3198 std::ptr::write_bytes(c.iomsg.add(copy), b' ', cap - copy);
3199 }
3200 }
3201 }
3202 }
3203 });
3204 }
3205
3206 fn advance_formatted_cursor(desc: &FormatDesc, input: &[u8], cursor: &mut usize) {
3207 match desc {
3208 FormatDesc::Skip { count } => {
3209 *cursor = (*cursor).saturating_add(*count).min(input.len());
3210 }
3211 FormatDesc::TabTo { position } => {
3212 *cursor = position.saturating_sub(1).min(input.len());
3213 }
3214 FormatDesc::TabLeft { count } => {
3215 *cursor = cursor.saturating_sub(*count);
3216 }
3217 FormatDesc::TabRight { count } => {
3218 *cursor = (*cursor).saturating_add(*count).min(input.len());
3219 }
3220 FormatDesc::LiteralString(text) => {
3221 *cursor = (*cursor).saturating_add(text.len()).min(input.len());
3222 }
3223 FormatDesc::Newline => {
3224 while *cursor < input.len() && input[*cursor] != b'\n' {
3225 *cursor += 1;
3226 }
3227 if *cursor < input.len() {
3228 *cursor += 1;
3229 }
3230 }
3231 _ => {}
3232 }
3233 }
3234
3235 fn read_formatted_field(desc: &FormatDesc, input: &[u8], cursor: &mut usize) -> Option<String> {
3236 let take_width = |cursor: &mut usize, width: usize| {
3237 let start = (*cursor).min(input.len());
3238 let end = start.saturating_add(width).min(input.len());
3239 *cursor = end;
3240 String::from_utf8_lossy(&input[start..end]).into_owned()
3241 };
3242
3243 match desc {
3244 FormatDesc::IntegerI { width, .. }
3245 | FormatDesc::IntegerB { width, .. }
3246 | FormatDesc::IntegerO { width, .. }
3247 | FormatDesc::IntegerZ { width, .. }
3248 | FormatDesc::RealF { width, .. }
3249 | FormatDesc::RealE { width, .. }
3250 | FormatDesc::RealEN { width, .. }
3251 | FormatDesc::RealES { width, .. }
3252 | FormatDesc::RealEX { width, .. }
3253 | FormatDesc::RealD { width, .. }
3254 | FormatDesc::RealG { width, .. }
3255 | FormatDesc::Logical { width } => Some(take_width(cursor, *width)),
3256 FormatDesc::Character { width: Some(width) } => Some(take_width(cursor, *width)),
3257 FormatDesc::Character { width: None } => {
3258 let start = *cursor;
3259 *cursor = input.len();
3260 Some(String::from_utf8_lossy(&input[start..]).into_owned())
3261 }
3262 _ => None,
3263 }
3264 }
3265
3266 fn extract_nth_formatted_field(
3267 descs: &[FormatDesc],
3268 input: &[u8],
3269 cursor: &mut usize,
3270 remaining_data_index: &mut usize,
3271 ) -> Option<(FormatDesc, String)> {
3272 for desc in descs {
3273 match desc {
3274 FormatDesc::Group {
3275 repeat,
3276 descriptors,
3277 } => {
3278 for _ in 0..*repeat {
3279 if let Some(found) = extract_nth_formatted_field(
3280 descriptors,
3281 input,
3282 cursor,
3283 remaining_data_index,
3284 ) {
3285 return Some(found);
3286 }
3287 }
3288 }
3289 FormatDesc::UnlimitedRepeat { descriptors } => {
3290 let mut loop_guard = 0usize;
3291 while *cursor < input.len() && loop_guard < input.len().saturating_add(1) {
3292 let before = *cursor;
3293 if let Some(found) = extract_nth_formatted_field(
3294 descriptors,
3295 input,
3296 cursor,
3297 remaining_data_index,
3298 ) {
3299 return Some(found);
3300 }
3301 if *cursor == before {
3302 break;
3303 }
3304 loop_guard += 1;
3305 }
3306 }
3307 _ => {
3308 if let Some(field) = read_formatted_field(desc, input, cursor) {
3309 if *remaining_data_index == 0 {
3310 return Some((desc.clone(), field));
3311 }
3312 *remaining_data_index -= 1;
3313 } else {
3314 advance_formatted_cursor(desc, input, cursor);
3315 }
3316 }
3317 }
3318 }
3319
3320 None
3321 }
3322
3323 fn parse_nth_formatted_record(
3324 input: &[u8],
3325 fmt_str: *const u8,
3326 fmt_len: i64,
3327 data_index: i64,
3328 ) -> Result<(FormatDesc, String), i32> {
3329 let fmt = unsafe_str(fmt_str, fmt_len);
3330 let descs = parse_format(&fmt);
3331 let mut cursor = 0usize;
3332 let mut remaining = data_index.max(0) as usize;
3333
3334 extract_nth_formatted_field(&descs, input, &mut cursor, &mut remaining).ok_or(-1)
3335 }
3336
3337 fn parse_nth_formatted_internal_field(
3338 buf: *const u8,
3339 buf_len: i64,
3340 fmt_str: *const u8,
3341 fmt_len: i64,
3342 data_index: i64,
3343 ) -> Result<(FormatDesc, String), i32> {
3344 if buf.is_null() || buf_len <= 0 {
3345 return Err(-1);
3346 }
3347
3348 let input = unsafe { std::slice::from_raw_parts(buf, buf_len as usize) };
3349 parse_nth_formatted_record(input, fmt_str, fmt_len, data_index)
3350 }
3351
3352 fn formatted_read_record_for_unit(unit: i32, data_index: i64) -> Result<String, i32> {
3353 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
3354 let Some(u) = state.get_unit(unit) else {
3355 return Err(1);
3356 };
3357
3358 if data_index <= 0 || u.formatted_read_record.is_none() {
3359 match u.read_line() {
3360 Ok(line) if !line.is_empty() => {
3361 u.formatted_read_record = Some(line);
3362 }
3363 Ok(_) => return Err(IOSTAT_END),
3364 Err(_) => return Err(1),
3365 }
3366 }
3367
3368 u.formatted_read_record
3369 .as_ref()
3370 .map(|line| line.trim_end_matches(['\r', '\n']).to_string())
3371 .ok_or(IOSTAT_END)
3372 }
3373
3374 fn store_formatted_char_result(
3375 field: &str,
3376 dest: *mut u8,
3377 dest_len: i64,
3378 size_out: *mut i32,
3379 iostat: *mut i32,
3380 ) {
3381 crate::string::afs_assign_char_fixed(dest, dest_len, field.as_ptr(), field.len() as i64);
3382 if !size_out.is_null() {
3383 unsafe {
3384 *size_out = field.len().min(i32::MAX as usize) as i32;
3385 }
3386 }
3387 if !iostat.is_null() {
3388 unsafe {
3389 *iostat = 0;
3390 }
3391 }
3392 }
3393
3394 fn store_formatted_char_error(dest: *mut u8, dest_len: i64, size_out: *mut i32, code: i32) {
3395 crate::string::afs_assign_char_fixed(dest, dest_len, std::ptr::null(), 0);
3396 if !size_out.is_null() {
3397 unsafe {
3398 *size_out = 0;
3399 }
3400 }
3401 if code != 0 {
3402 // Caller writes IOSTAT when it passed a non-null pointer.
3403 }
3404 }
3405
3406 fn parse_formatted_integer_field(desc: &FormatDesc, field: &str) -> Option<i128> {
3407 let trimmed = field.trim().replace(',', "");
3408 if trimmed.is_empty() {
3409 return None;
3410 }
3411 let (negative, digits) = if let Some(rest) = trimmed.strip_prefix('-') {
3412 (true, rest)
3413 } else if let Some(rest) = trimmed.strip_prefix('+') {
3414 (false, rest)
3415 } else {
3416 (false, trimmed.as_str())
3417 };
3418 let radix = match desc {
3419 FormatDesc::IntegerI { .. } => 10,
3420 FormatDesc::IntegerB { .. } => 2,
3421 FormatDesc::IntegerO { .. } => 8,
3422 FormatDesc::IntegerZ { .. } => 16,
3423 _ => return None,
3424 };
3425 let parsed = i128::from_str_radix(digits, radix).ok()?;
3426 Some(if negative { -parsed } else { parsed })
3427 }
3428
3429 #[no_mangle]
3430 pub extern "C" fn afs_fmt_read_string(
3431 unit: i32,
3432 fmt_str: *const u8,
3433 fmt_len: i64,
3434 data_index: i64,
3435 dest: *mut u8,
3436 dest_len: i64,
3437 size_out: *mut i32,
3438 iostat: *mut i32,
3439 ) {
3440 // If a prior `read(...,advance='NO')` left a partial in-flight
3441 // record with cursor in the middle, consume from that cursor first
3442 // and then mark the record as fully consumed (advancing past it
3443 // for the next statement). Without this, switching from
3444 // non-advancing to advancing — exactly what stdlib's
3445 // `read_bitset_unit_64` does on its final-bit read — would call
3446 // `read_line` and discard the cursor, returning the wrong char or
3447 // EOF for files with a single physical record.
3448 {
3449 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
3450 if let Some(u) = state.get_unit(unit) {
3451 // Only treat the in-flight record as partial when the
3452 // cursor has actually advanced past 0 — i.e. a previous
3453 // `read(...,advance='NO')` consumed some chars. A cursor
3454 // of 0 means the record was set up by `read_line` for an
3455 // advancing read but never partially consumed; in that
3456 // case we must let the next advancing read pull a fresh
3457 // record (otherwise we'd re-deliver the same line).
3458 let has_partial_record = u.formatted_read_cursor > 0
3459 && u.formatted_read_record.is_some()
3460 && u.formatted_read_cursor
3461 < u.formatted_read_record
3462 .as_ref()
3463 .map(|r| r.len())
3464 .unwrap_or(0);
3465 if has_partial_record {
3466 let fmt = unsafe_str(fmt_str, fmt_len);
3467 let descs = parse_format(&fmt);
3468 let input = u
3469 .formatted_read_record
3470 .as_ref()
3471 .map(|l| l.as_bytes().to_vec())
3472 .unwrap_or_default();
3473 let mut cursor = u.formatted_read_cursor;
3474 let mut remaining = 0usize;
3475 let outcome =
3476 extract_nth_formatted_field(&descs, &input, &mut cursor, &mut remaining);
3477 u.formatted_read_record = None;
3478 u.formatted_read_cursor = 0;
3479 drop(state);
3480 match outcome {
3481 Some((FormatDesc::Character { .. }, field)) => {
3482 store_formatted_char_result(&field, dest, dest_len, size_out, iostat);
3483 }
3484 _ => {
3485 store_formatted_char_error(dest, dest_len, size_out, IOSTAT_EOR);
3486 if !iostat.is_null() {
3487 unsafe {
3488 *iostat = IOSTAT_EOR;
3489 }
3490 }
3491 }
3492 }
3493 return;
3494 }
3495 }
3496 }
3497 match formatted_read_record_for_unit(unit, data_index)
3498 .and_then(|line| parse_nth_formatted_record(line.as_bytes(), fmt_str, fmt_len, data_index))
3499 {
3500 Ok((FormatDesc::Character { .. }, field)) => {
3501 store_formatted_char_result(&field, dest, dest_len, size_out, iostat);
3502 }
3503 Ok(_) => {
3504 store_formatted_char_error(dest, dest_len, size_out, 1);
3505 if !iostat.is_null() {
3506 unsafe {
3507 *iostat = 1;
3508 }
3509 }
3510 }
3511 Err(code) => {
3512 store_formatted_char_error(dest, dest_len, size_out, code);
3513 if !iostat.is_null() {
3514 unsafe {
3515 *iostat = code;
3516 }
3517 }
3518 }
3519 }
3520 }
3521
3522 #[no_mangle]
3523 pub extern "C" fn afs_fmt_read_string_noadvance(
3524 unit: i32,
3525 fmt_str: *const u8,
3526 fmt_len: i64,
3527 dest: *mut u8,
3528 dest_len: i64,
3529 size_out: *mut i32,
3530 iostat: *mut i32,
3531 ) {
3532 let fmt = unsafe_str(fmt_str, fmt_len);
3533 let descs = parse_format(&fmt);
3534
3535 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
3536 let Some(u) = state.get_unit(unit) else {
3537 store_formatted_char_error(dest, dest_len, size_out, 1);
3538 if !iostat.is_null() {
3539 unsafe {
3540 *iostat = 1;
3541 }
3542 }
3543 return;
3544 };
3545
3546 if u.formatted_read_record.is_none() {
3547 match u.read_line() {
3548 Ok(line) if !line.is_empty() => {
3549 u.formatted_read_record = Some(line.trim_end_matches(['\r', '\n']).to_string());
3550 u.formatted_read_cursor = 0;
3551 }
3552 Ok(_) => {
3553 store_formatted_char_error(dest, dest_len, size_out, IOSTAT_END);
3554 if !iostat.is_null() {
3555 unsafe {
3556 *iostat = IOSTAT_END;
3557 }
3558 }
3559 return;
3560 }
3561 Err(_) => {
3562 store_formatted_char_error(dest, dest_len, size_out, 1);
3563 if !iostat.is_null() {
3564 unsafe {
3565 *iostat = 1;
3566 }
3567 }
3568 return;
3569 }
3570 }
3571 }
3572
3573 let input = u
3574 .formatted_read_record
3575 .as_ref()
3576 .map(|line| line.as_bytes().to_vec())
3577 .unwrap_or_default();
3578 if u.formatted_read_cursor >= input.len() {
3579 store_formatted_char_error(dest, dest_len, size_out, IOSTAT_EOR);
3580 if !iostat.is_null() {
3581 unsafe {
3582 *iostat = IOSTAT_EOR;
3583 }
3584 }
3585 u.formatted_read_record = None;
3586 u.formatted_read_cursor = 0;
3587 return;
3588 }
3589 let mut cursor = u.formatted_read_cursor;
3590 let mut remaining = 0usize;
3591
3592 match extract_nth_formatted_field(&descs, &input, &mut cursor, &mut remaining) {
3593 Some((FormatDesc::Character { .. }, field)) => {
3594 store_formatted_char_result(&field, dest, dest_len, size_out, iostat);
3595 u.formatted_read_cursor = cursor;
3596 }
3597 Some(_) => {
3598 store_formatted_char_error(dest, dest_len, size_out, 1);
3599 if !iostat.is_null() {
3600 unsafe {
3601 *iostat = 1;
3602 }
3603 }
3604 }
3605 None => {
3606 store_formatted_char_error(dest, dest_len, size_out, IOSTAT_EOR);
3607 if !iostat.is_null() {
3608 unsafe {
3609 *iostat = IOSTAT_EOR;
3610 }
3611 }
3612 u.formatted_read_record = None;
3613 u.formatted_read_cursor = 0;
3614 }
3615 }
3616 }
3617
3618 #[no_mangle]
3619 pub extern "C" fn afs_fmt_read_int(
3620 unit: i32,
3621 fmt_str: *const u8,
3622 fmt_len: i64,
3623 data_index: i64,
3624 val: *mut i32,
3625 iostat: *mut i32,
3626 ) {
3627 match formatted_read_record_for_unit(unit, data_index)
3628 .and_then(|line| parse_nth_formatted_record(line.as_bytes(), fmt_str, fmt_len, data_index))
3629 {
3630 Ok((desc @ FormatDesc::IntegerI { .. }, field))
3631 | Ok((desc @ FormatDesc::IntegerB { .. }, field))
3632 | Ok((desc @ FormatDesc::IntegerO { .. }, field))
3633 | Ok((desc @ FormatDesc::IntegerZ { .. }, field)) => {
3634 match parse_formatted_integer_field(&desc, &field).and_then(|v| i32::try_from(v).ok()) {
3635 Some(v) => {
3636 if !val.is_null() {
3637 unsafe {
3638 *val = v;
3639 }
3640 }
3641 if !iostat.is_null() {
3642 unsafe {
3643 *iostat = 0;
3644 }
3645 }
3646 }
3647 None => {
3648 if !iostat.is_null() {
3649 unsafe {
3650 *iostat = 1;
3651 }
3652 }
3653 }
3654 }
3655 }
3656 Ok(_) => {
3657 if !iostat.is_null() {
3658 unsafe {
3659 *iostat = 1;
3660 }
3661 }
3662 }
3663 Err(code) => {
3664 if !iostat.is_null() {
3665 unsafe {
3666 *iostat = code;
3667 }
3668 }
3669 }
3670 }
3671 }
3672
3673 #[no_mangle]
3674 pub extern "C" fn afs_fmt_read_int64(
3675 unit: i32,
3676 fmt_str: *const u8,
3677 fmt_len: i64,
3678 data_index: i64,
3679 val: *mut i64,
3680 iostat: *mut i32,
3681 ) {
3682 match formatted_read_record_for_unit(unit, data_index)
3683 .and_then(|line| parse_nth_formatted_record(line.as_bytes(), fmt_str, fmt_len, data_index))
3684 {
3685 Ok((desc @ FormatDesc::IntegerI { .. }, field))
3686 | Ok((desc @ FormatDesc::IntegerB { .. }, field))
3687 | Ok((desc @ FormatDesc::IntegerO { .. }, field))
3688 | Ok((desc @ FormatDesc::IntegerZ { .. }, field)) => {
3689 match parse_formatted_integer_field(&desc, &field).and_then(|v| i64::try_from(v).ok()) {
3690 Some(v) => {
3691 if !val.is_null() {
3692 unsafe {
3693 *val = v;
3694 }
3695 }
3696 if !iostat.is_null() {
3697 unsafe {
3698 *iostat = 0;
3699 }
3700 }
3701 }
3702 None => {
3703 if !iostat.is_null() {
3704 unsafe {
3705 *iostat = 1;
3706 }
3707 }
3708 }
3709 }
3710 }
3711 Ok(_) => {
3712 if !iostat.is_null() {
3713 unsafe {
3714 *iostat = 1;
3715 }
3716 }
3717 }
3718 Err(code) => {
3719 if !iostat.is_null() {
3720 unsafe {
3721 *iostat = code;
3722 }
3723 }
3724 }
3725 }
3726 }
3727
3728 #[no_mangle]
3729 pub extern "C" fn afs_fmt_read_int128(
3730 unit: i32,
3731 fmt_str: *const u8,
3732 fmt_len: i64,
3733 data_index: i64,
3734 val: *mut i128,
3735 iostat: *mut i32,
3736 ) {
3737 match formatted_read_record_for_unit(unit, data_index)
3738 .and_then(|line| parse_nth_formatted_record(line.as_bytes(), fmt_str, fmt_len, data_index))
3739 {
3740 Ok((desc @ FormatDesc::IntegerI { .. }, field))
3741 | Ok((desc @ FormatDesc::IntegerB { .. }, field))
3742 | Ok((desc @ FormatDesc::IntegerO { .. }, field))
3743 | Ok((desc @ FormatDesc::IntegerZ { .. }, field)) => {
3744 match parse_formatted_integer_field(&desc, &field) {
3745 Some(v) => {
3746 write_i128_ptr(val, v);
3747 if !iostat.is_null() {
3748 unsafe {
3749 *iostat = 0;
3750 }
3751 }
3752 }
3753 None => {
3754 if !iostat.is_null() {
3755 unsafe {
3756 *iostat = 1;
3757 }
3758 }
3759 }
3760 }
3761 }
3762 Ok(_) => {
3763 if !iostat.is_null() {
3764 unsafe {
3765 *iostat = 1;
3766 }
3767 }
3768 }
3769 Err(code) => {
3770 if !iostat.is_null() {
3771 unsafe {
3772 *iostat = code;
3773 }
3774 }
3775 }
3776 }
3777 }
3778
3779 #[no_mangle]
3780 pub extern "C" fn afs_fmt_read_real(
3781 unit: i32,
3782 fmt_str: *const u8,
3783 fmt_len: i64,
3784 data_index: i64,
3785 val: *mut f64,
3786 iostat: *mut i32,
3787 ) {
3788 match formatted_read_record_for_unit(unit, data_index)
3789 .and_then(|line| parse_nth_formatted_record(line.as_bytes(), fmt_str, fmt_len, data_index))
3790 {
3791 Ok((FormatDesc::RealF { .. }, field))
3792 | Ok((FormatDesc::RealE { .. }, field))
3793 | Ok((FormatDesc::RealEN { .. }, field))
3794 | Ok((FormatDesc::RealES { .. }, field))
3795 | Ok((FormatDesc::RealEX { .. }, field))
3796 | Ok((FormatDesc::RealD { .. }, field))
3797 | Ok((FormatDesc::RealG { .. }, field)) => {
3798 let normalized = field
3799 .trim()
3800 .replace('d', "e")
3801 .replace('D', "E")
3802 .replace(',', "");
3803 match normalized.parse::<f64>() {
3804 Ok(v) => {
3805 if !val.is_null() {
3806 unsafe {
3807 *val = v;
3808 }
3809 }
3810 if !iostat.is_null() {
3811 unsafe {
3812 *iostat = 0;
3813 }
3814 }
3815 }
3816 Err(_) => {
3817 if !iostat.is_null() {
3818 unsafe {
3819 *iostat = 1;
3820 }
3821 }
3822 }
3823 }
3824 }
3825 Ok(_) => {
3826 if !iostat.is_null() {
3827 unsafe {
3828 *iostat = 1;
3829 }
3830 }
3831 }
3832 Err(code) => {
3833 if !iostat.is_null() {
3834 unsafe {
3835 *iostat = code;
3836 }
3837 }
3838 }
3839 }
3840 }
3841
3842 #[no_mangle]
3843 pub extern "C" fn afs_fmt_read_string_internal(
3844 buf: *const u8,
3845 buf_len: i64,
3846 fmt_str: *const u8,
3847 fmt_len: i64,
3848 data_index: i64,
3849 dest: *mut u8,
3850 dest_len: i64,
3851 size_out: *mut i32,
3852 iostat: *mut i32,
3853 ) {
3854 match parse_nth_formatted_internal_field(buf, buf_len, fmt_str, fmt_len, data_index) {
3855 Ok((FormatDesc::Character { .. }, field)) => {
3856 store_formatted_char_result(&field, dest, dest_len, size_out, iostat);
3857 }
3858 Ok(_) => {
3859 store_formatted_char_error(dest, dest_len, size_out, 1);
3860 if !iostat.is_null() {
3861 unsafe {
3862 *iostat = 1;
3863 }
3864 }
3865 }
3866 Err(code) => {
3867 store_formatted_char_error(dest, dest_len, size_out, code);
3868 if !iostat.is_null() {
3869 unsafe {
3870 *iostat = code;
3871 }
3872 }
3873 }
3874 }
3875 }
3876
3877 #[no_mangle]
3878 pub extern "C" fn afs_fmt_read_int_internal(
3879 buf: *const u8,
3880 buf_len: i64,
3881 fmt_str: *const u8,
3882 fmt_len: i64,
3883 data_index: i64,
3884 val: *mut i32,
3885 iostat: *mut i32,
3886 ) {
3887 match parse_nth_formatted_internal_field(buf, buf_len, fmt_str, fmt_len, data_index) {
3888 Ok((desc @ FormatDesc::IntegerI { .. }, field))
3889 | Ok((desc @ FormatDesc::IntegerB { .. }, field))
3890 | Ok((desc @ FormatDesc::IntegerO { .. }, field))
3891 | Ok((desc @ FormatDesc::IntegerZ { .. }, field)) => {
3892 match parse_formatted_integer_field(&desc, &field).and_then(|v| i32::try_from(v).ok()) {
3893 Some(v) => {
3894 if !val.is_null() {
3895 unsafe {
3896 *val = v;
3897 }
3898 }
3899 if !iostat.is_null() {
3900 unsafe {
3901 *iostat = 0;
3902 }
3903 }
3904 }
3905 None => {
3906 if !iostat.is_null() {
3907 unsafe {
3908 *iostat = 1;
3909 }
3910 }
3911 }
3912 }
3913 }
3914 Ok(_) => {
3915 if !iostat.is_null() {
3916 unsafe {
3917 *iostat = 1;
3918 }
3919 }
3920 }
3921 Err(code) => {
3922 if !iostat.is_null() {
3923 unsafe {
3924 *iostat = code;
3925 }
3926 }
3927 }
3928 }
3929 }
3930
3931 #[no_mangle]
3932 pub extern "C" fn afs_fmt_read_int64_internal(
3933 buf: *const u8,
3934 buf_len: i64,
3935 fmt_str: *const u8,
3936 fmt_len: i64,
3937 data_index: i64,
3938 val: *mut i64,
3939 iostat: *mut i32,
3940 ) {
3941 match parse_nth_formatted_internal_field(buf, buf_len, fmt_str, fmt_len, data_index) {
3942 Ok((desc @ FormatDesc::IntegerI { .. }, field))
3943 | Ok((desc @ FormatDesc::IntegerB { .. }, field))
3944 | Ok((desc @ FormatDesc::IntegerO { .. }, field))
3945 | Ok((desc @ FormatDesc::IntegerZ { .. }, field)) => {
3946 match parse_formatted_integer_field(&desc, &field).and_then(|v| i64::try_from(v).ok()) {
3947 Some(v) => {
3948 if !val.is_null() {
3949 unsafe {
3950 *val = v;
3951 }
3952 }
3953 if !iostat.is_null() {
3954 unsafe {
3955 *iostat = 0;
3956 }
3957 }
3958 }
3959 None => {
3960 if !iostat.is_null() {
3961 unsafe {
3962 *iostat = 1;
3963 }
3964 }
3965 }
3966 }
3967 }
3968 Ok(_) => {
3969 if !iostat.is_null() {
3970 unsafe {
3971 *iostat = 1;
3972 }
3973 }
3974 }
3975 Err(code) => {
3976 if !iostat.is_null() {
3977 unsafe {
3978 *iostat = code;
3979 }
3980 }
3981 }
3982 }
3983 }
3984
3985 #[no_mangle]
3986 pub extern "C" fn afs_fmt_read_int128_internal(
3987 buf: *const u8,
3988 buf_len: i64,
3989 fmt_str: *const u8,
3990 fmt_len: i64,
3991 data_index: i64,
3992 val: *mut i128,
3993 iostat: *mut i32,
3994 ) {
3995 match parse_nth_formatted_internal_field(buf, buf_len, fmt_str, fmt_len, data_index) {
3996 Ok((desc @ FormatDesc::IntegerI { .. }, field))
3997 | Ok((desc @ FormatDesc::IntegerB { .. }, field))
3998 | Ok((desc @ FormatDesc::IntegerO { .. }, field))
3999 | Ok((desc @ FormatDesc::IntegerZ { .. }, field)) => {
4000 match parse_formatted_integer_field(&desc, &field) {
4001 Some(v) => {
4002 write_i128_ptr(val, v);
4003 if !iostat.is_null() {
4004 unsafe {
4005 *iostat = 0;
4006 }
4007 }
4008 }
4009 None => {
4010 if !iostat.is_null() {
4011 unsafe {
4012 *iostat = 1;
4013 }
4014 }
4015 }
4016 }
4017 }
4018 Ok(_) => {
4019 if !iostat.is_null() {
4020 unsafe {
4021 *iostat = 1;
4022 }
4023 }
4024 }
4025 Err(code) => {
4026 if !iostat.is_null() {
4027 unsafe {
4028 *iostat = code;
4029 }
4030 }
4031 }
4032 }
4033 }
4034
4035 #[no_mangle]
4036 pub extern "C" fn afs_fmt_read_real_internal(
4037 buf: *const u8,
4038 buf_len: i64,
4039 fmt_str: *const u8,
4040 fmt_len: i64,
4041 data_index: i64,
4042 val: *mut f64,
4043 iostat: *mut i32,
4044 ) {
4045 match parse_nth_formatted_internal_field(buf, buf_len, fmt_str, fmt_len, data_index) {
4046 Ok((FormatDesc::RealF { .. }, field))
4047 | Ok((FormatDesc::RealE { .. }, field))
4048 | Ok((FormatDesc::RealEN { .. }, field))
4049 | Ok((FormatDesc::RealES { .. }, field))
4050 | Ok((FormatDesc::RealEX { .. }, field))
4051 | Ok((FormatDesc::RealD { .. }, field))
4052 | Ok((FormatDesc::RealG { .. }, field)) => {
4053 let normalized = field
4054 .trim()
4055 .replace('d', "e")
4056 .replace('D', "E")
4057 .replace(',', "");
4058 match normalized.parse::<f64>() {
4059 Ok(v) => {
4060 if !val.is_null() {
4061 unsafe {
4062 *val = v;
4063 }
4064 }
4065 if !iostat.is_null() {
4066 unsafe {
4067 *iostat = 0;
4068 }
4069 }
4070 }
4071 Err(_) => {
4072 if !iostat.is_null() {
4073 unsafe {
4074 *iostat = 1;
4075 }
4076 }
4077 }
4078 }
4079 }
4080 Ok(_) => {
4081 if !iostat.is_null() {
4082 unsafe {
4083 *iostat = 1;
4084 }
4085 }
4086 }
4087 Err(code) => {
4088 if !iostat.is_null() {
4089 unsafe {
4090 *iostat = code;
4091 }
4092 }
4093 }
4094 }
4095 }
4096
4097 #[cfg(test)]
4098 mod tests {
4099 use super::*;
4100
4101 #[test]
4102 fn preconnected_units() {
4103 let state = io_state().lock().unwrap_or_else(|e| e.into_inner());
4104 assert!(state.units.contains_key(&0)); // stderr
4105 assert!(state.units.contains_key(&5)); // stdin
4106 assert!(state.units.contains_key(&6)); // stdout
4107 }
4108
4109 #[test]
4110 fn newunit_allocation() {
4111 let mut state = io_state().lock().unwrap_or_else(|e| e.into_inner());
4112 let u1 = state.alloc_newunit();
4113 let u2 = state.alloc_newunit();
4114 assert!(u1 < 0); // negative unit numbers
4115 assert_ne!(u1, u2);
4116 }
4117
4118 #[test]
4119 fn write_to_stdout() {
4120 // This test just verifies no panic — output goes to test runner's stdout.
4121 afs_write_int(6, 42);
4122 afs_write_newline(6);
4123 }
4124
4125 #[test]
4126 fn stream_unformatted_string_write_preserves_exact_bytes() {
4127 let path = "/tmp/afs_stream_unformatted_string_write.dat";
4128 let mut iostat = -99i32;
4129 let cb = OpenControlBlock {
4130 unit: 94,
4131 filename: path.as_ptr(),
4132 filename_len: path.len() as i64,
4133 status: "replace".as_ptr(),
4134 status_len: 7,
4135 action: "write".as_ptr(),
4136 action_len: 5,
4137 access: "stream".as_ptr(),
4138 access_len: 6,
4139 form: "unformatted".as_ptr(),
4140 form_len: 11,
4141 recl: 0,
4142 iostat: &mut iostat,
4143 newunit: std::ptr::null_mut(),
4144 position: std::ptr::null(),
4145 position_len: 0,
4146 };
4147
4148 afs_open(&cb);
4149 assert_eq!(iostat, 0, "expected stream-unformatted OPEN to succeed");
4150
4151 afs_write_string(94, "alpha".as_ptr(), 5);
4152 afs_write_newline(94);
4153 afs_close(94, std::ptr::null_mut());
4154
4155 let content = std::fs::read(path).unwrap();
4156 assert_eq!(content, b"alpha", "expected exact stream bytes");
4157 }
4158
4159 #[test]
4160 fn write_i128_to_file() {
4161 let path = "/tmp/afs_write_i128_test.dat";
4162 afs_open_simple(
4163 97,
4164 path.as_ptr(),
4165 path.len() as i64,
4166 "replace".as_ptr(),
4167 7,
4168 std::ptr::null(),
4169 0,
4170 );
4171
4172 afs_write_int128(97, 170141183460469231731687303715884105727i128);
4173 afs_write_newline(97);
4174 afs_close(97, std::ptr::null_mut());
4175
4176 let content = std::fs::read_to_string(path).unwrap();
4177 assert!(
4178 content.contains("170141183460469231731687303715884105727"),
4179 "expected full i128 decimal rendering in: {}",
4180 content
4181 );
4182 }
4183
4184 #[test]
4185 fn read_i128_from_file() {
4186 let path = "/tmp/afs_read_i128_test.dat";
4187 std::fs::write(path, "170141183460469231731687303715884105727\n").unwrap();
4188
4189 afs_open_simple(
4190 95,
4191 path.as_ptr(),
4192 path.len() as i64,
4193 "old".as_ptr(),
4194 3,
4195 "read".as_ptr(),
4196 4,
4197 );
4198
4199 let mut value = 0i128;
4200 let mut iostat = -99i32;
4201 afs_read_int128(95, &mut value, &mut iostat);
4202 afs_close(95, std::ptr::null_mut());
4203
4204 assert_eq!(iostat, 0, "expected successful i128 read");
4205 assert_eq!(value, 170141183460469231731687303715884105727i128);
4206 }
4207
4208 #[test]
4209 fn internal_i128_roundtrip_tracks_position() {
4210 let mut buf = [b' '; 96];
4211 let mut write_pos = 0i64;
4212
4213 afs_write_internal_int128(
4214 buf.as_mut_ptr(),
4215 buf.len() as i64,
4216 170141183460469231731687303715884105727i128,
4217 &mut write_pos,
4218 );
4219 afs_write_internal_int128(
4220 buf.as_mut_ptr(),
4221 buf.len() as i64,
4222 -170141183460469231731687303715884105727i128,
4223 &mut write_pos,
4224 );
4225
4226 let mut read_pos = 0i64;
4227 let mut first = 0i128;
4228 let mut second = 0i128;
4229 let mut iostat = -99i32;
4230
4231 afs_read_internal_int128(
4232 buf.as_ptr(),
4233 buf.len() as i64,
4234 &mut read_pos,
4235 &mut first,
4236 &mut iostat,
4237 );
4238 assert_eq!(iostat, 0, "expected first internal i128 read to succeed");
4239
4240 afs_read_internal_int128(
4241 buf.as_ptr(),
4242 buf.len() as i64,
4243 &mut read_pos,
4244 &mut second,
4245 &mut iostat,
4246 );
4247 assert_eq!(iostat, 0, "expected second internal i128 read to succeed");
4248 assert_eq!(first, 170141183460469231731687303715884105727i128);
4249 assert_eq!(second, -170141183460469231731687303715884105727i128);
4250 }
4251
4252 #[test]
4253 fn internal_i128_read_accepts_unaligned_destination() {
4254 let buf = b"170141183460469231731687303715884105727";
4255 let mut pos = 0i64;
4256 let mut raw = [0u8; 32];
4257 let ptr = unsafe { raw.as_mut_ptr().add(1) as *mut i128 };
4258 let mut iostat = -99i32;
4259
4260 afs_read_internal_int128(buf.as_ptr(), buf.len() as i64, &mut pos, ptr, &mut iostat);
4261
4262 assert_eq!(iostat, 0, "expected internal i128 read to succeed");
4263 let value = unsafe { std::ptr::read_unaligned(ptr) };
4264 assert_eq!(value, 170141183460469231731687303715884105727i128);
4265 }
4266
4267 #[test]
4268 fn formatted_write_to_file() {
4269 let path = "/tmp/afs_fmt_test.dat";
4270 afs_open_simple(
4271 99,
4272 path.as_ptr(),
4273 path.len() as i64,
4274 "replace".as_ptr(),
4275 7,
4276 std::ptr::null(),
4277 0,
4278 );
4279
4280 afs_fmt_begin(99, "(I5, F8.2)".as_ptr(), 10);
4281 afs_fmt_push_int(42);
4282 afs_fmt_push_real(3.14);
4283 afs_fmt_end(1); // with newline
4284
4285 afs_close(99, std::ptr::null_mut());
4286
4287 let content = std::fs::read_to_string(path).unwrap();
4288 assert!(content.contains("42"), "expected 42 in: {}", content);
4289 assert!(content.contains("3.14"), "expected 3.14 in: {}", content);
4290 }
4291
4292 #[test]
4293 fn formatted_write_integer16_to_file() {
4294 let path = "/tmp/afs_fmt_i128_test.dat";
4295 let wide = 170141183460469231731687303715884105727i128;
4296 afs_open_simple(
4297 96,
4298 path.as_ptr(),
4299 path.len() as i64,
4300 "replace".as_ptr(),
4301 7,
4302 std::ptr::null(),
4303 0,
4304 );
4305
4306 afs_fmt_begin(96, "(I40)".as_ptr(), 5);
4307 afs_fmt_push_int128(&wide);
4308 afs_fmt_end(1);
4309
4310 afs_close(96, std::ptr::null_mut());
4311
4312 let content = std::fs::read_to_string(path).unwrap();
4313 assert!(
4314 content.contains("170141183460469231731687303715884105727"),
4315 "expected full formatted i128 rendering in: {}",
4316 content
4317 );
4318 }
4319
4320 #[test]
4321 fn formatted_write_integer16_accepts_unaligned_pointer() {
4322 let mut rendered = [b' '; 64];
4323 let mut raw = [0u8; 32];
4324 let wide = 170141183460469231731687303715884105727i128;
4325 let ptr = unsafe { raw.as_mut_ptr().add(1) as *mut i128 };
4326
4327 unsafe { std::ptr::write_unaligned(ptr, wide) };
4328
4329 afs_fmt_begin_internal(
4330 rendered.as_mut_ptr(),
4331 rendered.len() as i64,
4332 "(I40)".as_ptr(),
4333 5,
4334 );
4335 afs_fmt_push_int128(ptr);
4336 afs_fmt_end(0);
4337
4338 let text = String::from_utf8_lossy(&rendered).into_owned();
4339 assert!(
4340 text.contains("170141183460469231731687303715884105727"),
4341 "expected formatted internal write to accept unaligned i128 pointer: {:?}",
4342 text
4343 );
4344 }
4345
4346 #[test]
4347 fn formatted_internal_write_pads_buffer() {
4348 let mut buf = [b'?'; 48];
4349
4350 afs_fmt_begin_internal(buf.as_mut_ptr(), buf.len() as i64, "(I6)".as_ptr(), 4);
4351 afs_fmt_push_int(42);
4352 afs_fmt_end(0);
4353
4354 let rendered = String::from_utf8_lossy(&buf).into_owned();
4355 assert!(
4356 rendered.starts_with(" 42"),
4357 "expected formatted internal output at start of buffer: {:?}",
4358 rendered
4359 );
4360 assert!(
4361 rendered[6..].bytes().all(|b| b == b' '),
4362 "expected remaining internal buffer to be space padded: {:?}",
4363 rendered
4364 );
4365 }
4366
4367 #[test]
4368 fn formatted_internal_read_i128_field() {
4369 let buf = b" 170141183460469231731687303715884105727";
4370 let mut value = 0i128;
4371 let mut iostat = -99i32;
4372
4373 afs_fmt_read_int128_internal(
4374 buf.as_ptr(),
4375 buf.len() as i64,
4376 "(I40)".as_ptr(),
4377 5,
4378 0,
4379 &mut value,
4380 &mut iostat,
4381 );
4382
4383 assert_eq!(
4384 iostat, 0,
4385 "expected formatted internal i128 read to succeed"
4386 );
4387 assert_eq!(value, 170141183460469231731687303715884105727i128);
4388 }
4389
4390 #[test]
4391 fn formatted_internal_read_i128_accepts_unaligned_destination() {
4392 let buf = b" 170141183460469231731687303715884105727";
4393 let mut raw = [0u8; 32];
4394 let ptr = unsafe { raw.as_mut_ptr().add(1) as *mut i128 };
4395 let mut iostat = -99i32;
4396
4397 afs_fmt_read_int128_internal(
4398 buf.as_ptr(),
4399 buf.len() as i64,
4400 "(I40)".as_ptr(),
4401 5,
4402 0,
4403 ptr,
4404 &mut iostat,
4405 );
4406
4407 assert_eq!(
4408 iostat, 0,
4409 "expected formatted internal i128 read to succeed"
4410 );
4411 let value = unsafe { std::ptr::read_unaligned(ptr) };
4412 assert_eq!(value, 170141183460469231731687303715884105727i128);
4413 }
4414
4415 #[test]
4416 fn formatted_internal_read_tracks_descriptor_index() {
4417 let buf = b" 42 7";
4418 let mut first = 0i32;
4419 let mut second = 0i32;
4420 let mut iostat = -99i32;
4421
4422 afs_fmt_read_int_internal(
4423 buf.as_ptr(),
4424 buf.len() as i64,
4425 "(I4,1X,I1)".as_ptr(),
4426 10,
4427 0,
4428 &mut first,
4429 &mut iostat,
4430 );
4431 assert_eq!(iostat, 0);
4432
4433 afs_fmt_read_int_internal(
4434 buf.as_ptr(),
4435 buf.len() as i64,
4436 "(I4,1X,I1)".as_ptr(),
4437 10,
4438 1,
4439 &mut second,
4440 &mut iostat,
4441 );
4442 assert_eq!(iostat, 0);
4443 assert_eq!(first, 42);
4444 assert_eq!(second, 7);
4445 }
4446
4447 #[test]
4448 fn formatted_internal_read_supports_octal_descriptor() {
4449 let buf = b"077 22";
4450 let mut first = 0i32;
4451 let mut second = 0i32;
4452 let mut iostat = -99i32;
4453
4454 afs_fmt_read_int_internal(
4455 buf.as_ptr(),
4456 buf.len() as i64,
4457 "(O3,1X,O2)".as_ptr(),
4458 10,
4459 0,
4460 &mut first,
4461 &mut iostat,
4462 );
4463 assert_eq!(iostat, 0);
4464
4465 afs_fmt_read_int_internal(
4466 buf.as_ptr(),
4467 buf.len() as i64,
4468 "(O3,1X,O2)".as_ptr(),
4469 10,
4470 1,
4471 &mut second,
4472 &mut iostat,
4473 );
4474 assert_eq!(iostat, 0);
4475 assert_eq!(first, 63);
4476 assert_eq!(second, 18);
4477 }
4478
4479 #[test]
4480 fn formatted_unit_read_i128_field() {
4481 let path = "/tmp/afs_fmt_read_i128_test.dat";
4482 std::fs::write(path, " 170141183460469231731687303715884105727\n").unwrap();
4483
4484 afs_open_simple(
4485 94,
4486 path.as_ptr(),
4487 path.len() as i64,
4488 "old".as_ptr(),
4489 3,
4490 "read".as_ptr(),
4491 4,
4492 );
4493
4494 let mut value = 0i128;
4495 let mut iostat = -99i32;
4496 afs_fmt_read_int128(94, "(I40)".as_ptr(), 5, 0, &mut value, &mut iostat);
4497 afs_close(94, std::ptr::null_mut());
4498
4499 assert_eq!(iostat, 0, "expected formatted unit i128 read to succeed");
4500 assert_eq!(value, 170141183460469231731687303715884105727i128);
4501 }
4502
4503 #[test]
4504 fn formatted_unit_read_tracks_descriptor_index() {
4505 let path = "/tmp/afs_fmt_read_multi_test.dat";
4506 std::fs::write(path, " 170141183460469231731687303715884105727 42\n").unwrap();
4507
4508 afs_open_simple(
4509 93,
4510 path.as_ptr(),
4511 path.len() as i64,
4512 "old".as_ptr(),
4513 3,
4514 "read".as_ptr(),
4515 4,
4516 );
4517
4518 let mut first = 0i128;
4519 let mut second = 0i32;
4520 let mut iostat = -99i32;
4521 afs_fmt_read_int128(93, "(I40,1X,I4)".as_ptr(), 10, 0, &mut first, &mut iostat);
4522 assert_eq!(iostat, 0);
4523
4524 afs_fmt_read_int(93, "(I40,1X,I4)".as_ptr(), 10, 1, &mut second, &mut iostat);
4525 afs_close(93, std::ptr::null_mut());
4526
4527 assert_eq!(iostat, 0);
4528 assert_eq!(first, 170141183460469231731687303715884105727i128);
4529 assert_eq!(second, 42);
4530 }
4531
4532 #[test]
4533 fn formatted_readwrite_unit_advances_across_records() {
4534 let path = "/tmp/afs_fmt_read_records_rw_test.dat";
4535 std::fs::write(
4536 path,
4537 " 170141183460469231731687303715884105727\n-170141183460469231731687303715884105727\n",
4538 )
4539 .unwrap();
4540
4541 afs_open_simple(
4542 92,
4543 path.as_ptr(),
4544 path.len() as i64,
4545 "old".as_ptr(),
4546 3,
4547 "readwrite".as_ptr(),
4548 9,
4549 );
4550
4551 let mut first = 0i128;
4552 let mut second = 0i128;
4553 let mut iostat = -99i32;
4554
4555 afs_fmt_read_int128(92, "(I40)".as_ptr(), 5, 0, &mut first, &mut iostat);
4556 assert_eq!(
4557 iostat, 0,
4558 "expected first formatted readwrite-unit read to succeed"
4559 );
4560
4561 afs_fmt_read_int128(92, "(I40)".as_ptr(), 5, 0, &mut second, &mut iostat);
4562 afs_close(92, std::ptr::null_mut());
4563
4564 assert_eq!(
4565 iostat, 0,
4566 "expected second formatted readwrite-unit read to succeed"
4567 );
4568 assert_eq!(first, 170141183460469231731687303715884105727i128);
4569 assert_eq!(second, -170141183460469231731687303715884105727i128);
4570 }
4571
4572 #[test]
4573 fn formatted_write_no_advance() {
4574 let path = "/tmp/afs_fmt_noadv_test.dat";
4575 afs_open_simple(
4576 98,
4577 path.as_ptr(),
4578 path.len() as i64,
4579 "replace".as_ptr(),
4580 7,
4581 std::ptr::null(),
4582 0,
4583 );
4584
4585 afs_fmt_begin(98, "('hello')".as_ptr(), 9);
4586 afs_fmt_end(0); // no newline
4587
4588 afs_fmt_begin(98, "(' world')".as_ptr(), 10);
4589 afs_fmt_end(1); // with newline
4590
4591 afs_close(98, std::ptr::null_mut());
4592
4593 let content = std::fs::read_to_string(path).unwrap();
4594 assert_eq!(content.trim(), "hello world");
4595 }
4596 }
4597