relibc/header/stdio/getdelim.rs
1// See <https://pubs.opengroup.org/onlinepubs/9799919799/functions/getline.html>.
2
3use alloc::vec::Vec;
4use core::{intrinsics::unlikely, ptr};
5
6use crate::{
7 header::{
8 errno::{EINVAL, ENOMEM, EOVERFLOW},
9 stdio::FILE,
10 stdlib,
11 },
12 io::BufRead,
13 platform::types::{c_char, c_int, c_void, size_t, ssize_t},
14};
15
16use crate::{
17 header::stdio::{F_EOF, F_ERR, feof, ferror},
18 platform::ERRNO,
19};
20
21/// see getdelim (getline is a special case of getdelim with delim == '\n')
22#[unsafe(no_mangle)]
23pub unsafe extern "C" fn getline(
24 lineptr: *mut *mut c_char,
25 n: *mut size_t,
26 stream: *mut FILE,
27) -> ssize_t {
28 unsafe { getdelim(lineptr, n, c_int::from(b'\n'), stream) }
29}
30
31// One *could* read the standard as 'getdelim sets the stream error flag on *any* error, though
32// since glibc doesn't seem to do this, I won't either
33
34/// <https://pubs.opengroup.org/onlinepubs/9799919799/functions/getline.html>
35///
36/// # Safety
37/// - `lineptr, *lineptr, `n`, `stream` pointers must be valid and have to be aligned.
38/// - `stream` has to be a valid file handle returned by fopen and likes.
39///
40/// # Deviation from POSIX
41/// - **EINVAL is set on stream being NULL or delim not fitting into char**
42/// (POSIX allows UB)
43/// - **`*n` can contain invalid data.** The buffer size `n` is not read,
44/// instead realloc is called each time. That is in principle inefficent
45/// since the buffer is reallocated in memory for every call, but if `n` is
46/// by mistake bigger than the number of bytes allocated for the buffer,
47/// there can be no out-of-bounds write.
48/// - On non-stream-related errors, the error indicator of the stream is *not*
49/// set. Posix states "If an error occurs, the error indicator for the stream
50/// shall be set, and the function shall return -1 and set errno to indicate
51/// the error." but in cases that produce EINVAL even glibc doesn't seem to
52/// set the error indicator, so we also don't.
53#[unsafe(no_mangle)]
54pub unsafe extern "C" fn getdelim(
55 lineptr: *mut *mut c_char,
56 n: *mut size_t,
57 delim: c_int,
58 stream: *mut FILE,
59) -> ssize_t {
60 let (lineptr, n, stream) = if let (Some(ptr), Some(n), Some(file)) =
61 (unsafe { lineptr.as_mut() }, unsafe { n.as_mut() }, unsafe {
62 stream.as_mut()
63 }) {
64 (ptr, n, file)
65 } else {
66 ERRNO.set(EINVAL);
67 return -1 as ssize_t;
68 };
69
70 if unsafe { feof(stream) } != 0 || unsafe { ferror(stream) } != 0 {
71 return -1 as ssize_t;
72 }
73
74 // POSIX specifies UB but we test anyway
75 // returning EINVAL in that case
76 let delim: u8 = if let Ok(delim) = delim.try_into() {
77 delim
78 } else {
79 ERRNO.set(EINVAL);
80 return -1;
81 };
82
83 //TODO: More efficient algorithm using lineptr and n instead of this vec
84 let mut buf = Vec::new();
85 let count = {
86 let mut stream = (*stream).lock();
87 match stream.read_until(delim, &mut buf) {
88 Ok(ok) => ok,
89 Err(err) => {
90 stream.flags &= F_ERR;
91 return -1;
92 }
93 }
94 };
95
96 // "[EOVERFLOW]
97 // The number of bytes to be written into the buffer, including the delimiter character (if encountered), would exceed {SSIZE_MAX}."
98 if unlikely(count > ssize_t::MAX as usize) {
99 ERRNO.set(EOVERFLOW);
100 return -1;
101 }
102
103 // we reached EOF if either
104 // - we have no last elem (because vec is empty), or
105 // - the last elem doesn't match the delimiter
106 let eof_reached = if let Some(last) = buf.last() {
107 *last == delim
108 } else {
109 true
110 };
111
112 // "If the end-of-file indicator for the stream is set, or if no characters were read and the
113 // stream is at end-of-file, the end-of-file indicator for the stream shall be set and the
114 // function shall return -1."
115 if eof_reached {
116 stream.flags &= F_EOF;
117 if count == 0 {
118 return -1;
119 }
120 }
121
122 //TODO: Check errors and improve safety
123 {
124 // Allocate lineptr to size of buf plus NUL byte and set n to size of lineptr
125 *n = count + 1;
126 // The advantage in always realloc'ing is that even if the user supplies a wrong n, this
127 // doesn't break
128 *lineptr = unsafe { stdlib::realloc((*lineptr).cast::<c_void>(), *n) }.cast::<c_char>();
129 if unlikely(lineptr.is_null() && *n != 0usize) {
130 // memory error; realloc returns NULL on alloc'ing 0 bytes
131 ERRNO.set(ENOMEM);
132 return -1;
133 }
134
135 // Copy buf to lineptr
136 unsafe { ptr::copy(buf.as_ptr(), (*lineptr).cast::<u8>(), count) };
137
138 // NUL terminate lineptr
139 unsafe { *lineptr.add(count) = 0 };
140
141 // TODO remove
142 /*eprintln!(
143 "[DBG]{}: {}, {:?}, {:?}, {:?}", line!(),
144 String::from_utf8(buf).unwrap(), count, *n, *lineptr
145 );*/
146 // Return allocated size
147 count as ssize_t
148 }
149}