1use std::io::{self, Read, Write};
2use std::path::Path;
3
4use memchr::memchr_iter;
5
6use crate::common::io::{FileData, read_file, read_stdin};
7
8#[derive(Clone, Debug)]
10pub enum HeadMode {
11 Lines(u64),
13 LinesFromEnd(u64),
15 Bytes(u64),
17 BytesFromEnd(u64),
19}
20
21#[derive(Clone, Debug)]
23pub struct HeadConfig {
24 pub mode: HeadMode,
25 pub zero_terminated: bool,
26}
27
28impl Default for HeadConfig {
29 fn default() -> Self {
30 Self {
31 mode: HeadMode::Lines(10),
32 zero_terminated: false,
33 }
34 }
35}
36
37pub fn parse_size(s: &str) -> Result<u64, String> {
41 let s = s.trim();
42 if s.is_empty() {
43 return Err("empty size".to_string());
44 }
45
46 let mut num_end = 0;
48 for (i, c) in s.char_indices() {
49 if c.is_ascii_digit() || (i == 0 && (c == '+' || c == '-')) {
50 num_end = i + c.len_utf8();
51 } else {
52 break;
53 }
54 }
55
56 if num_end == 0 {
57 return Err(format!("invalid number: '{}'", s));
58 }
59
60 let num_str = &s[..num_end];
61 let suffix = &s[num_end..];
62
63 let num: u64 = num_str
64 .parse()
65 .map_err(|_| format!("invalid number: '{}'", num_str))?;
66
67 let multiplier: u64 = match suffix {
68 "" => 1,
69 "b" => 512,
70 "kB" => 1000,
71 "K" | "KiB" => 1024,
72 "MB" => 1_000_000,
73 "M" | "MiB" => 1_048_576,
74 "GB" => 1_000_000_000,
75 "G" | "GiB" => 1_073_741_824,
76 "TB" => 1_000_000_000_000,
77 "T" | "TiB" => 1_099_511_627_776,
78 "PB" => 1_000_000_000_000_000,
79 "P" | "PiB" => 1_125_899_906_842_624,
80 "EB" => 1_000_000_000_000_000_000,
81 "E" | "EiB" => 1_152_921_504_606_846_976,
82 "ZB" | "Z" | "ZiB" | "YB" | "Y" | "YiB" => {
84 if num > 0 {
85 return Ok(u64::MAX);
86 }
87 return Ok(0);
88 }
89 _ => return Err(format!("invalid suffix in '{}'", s)),
90 };
91
92 num.checked_mul(multiplier)
93 .ok_or_else(|| format!("number too large: '{}'", s))
94}
95
96pub fn head_lines(data: &[u8], n: u64, delimiter: u8, out: &mut impl Write) -> io::Result<()> {
98 if n == 0 || data.is_empty() {
99 return Ok(());
100 }
101
102 let mut count = 0u64;
103 for pos in memchr_iter(delimiter, data) {
104 count += 1;
105 if count == n {
106 return out.write_all(&data[..=pos]);
107 }
108 }
109
110 out.write_all(data)
112}
113
114pub fn head_lines_from_end(
116 data: &[u8],
117 n: u64,
118 delimiter: u8,
119 out: &mut impl Write,
120) -> io::Result<()> {
121 if n == 0 {
122 return out.write_all(data);
123 }
124 if data.is_empty() {
125 return Ok(());
126 }
127
128 let total_lines: u64 = memchr_iter(delimiter, data).count() as u64;
130
131 if n >= total_lines {
132 return Ok(());
133 }
134
135 let target = total_lines - n;
136 head_lines(data, target, delimiter, out)
137}
138
139pub fn head_bytes(data: &[u8], n: u64, out: &mut impl Write) -> io::Result<()> {
141 let n = n.min(data.len() as u64) as usize;
142 if n > 0 {
143 out.write_all(&data[..n])?;
144 }
145 Ok(())
146}
147
148pub fn head_bytes_from_end(data: &[u8], n: u64, out: &mut impl Write) -> io::Result<()> {
150 if n >= data.len() as u64 {
151 return Ok(());
152 }
153 let end = data.len() - n as usize;
154 if end > 0 {
155 out.write_all(&data[..end])?;
156 }
157 Ok(())
158}
159
160#[cfg(target_os = "linux")]
162pub fn sendfile_bytes(path: &Path, n: u64, out_fd: i32) -> io::Result<bool> {
163 use std::os::unix::fs::OpenOptionsExt;
164
165 let file = std::fs::OpenOptions::new()
166 .read(true)
167 .custom_flags(libc::O_NOATIME)
168 .open(path)
169 .or_else(|_| std::fs::File::open(path))?;
170
171 let metadata = file.metadata()?;
172 let file_size = metadata.len();
173 let to_send = n.min(file_size) as usize;
174
175 if to_send == 0 {
176 return Ok(true);
177 }
178
179 use std::os::unix::io::AsRawFd;
180 let in_fd = file.as_raw_fd();
181 let mut offset: libc::off_t = 0;
182 let mut remaining = to_send;
183
184 while remaining > 0 {
185 let chunk = remaining.min(0x7ffff000); let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
187 if ret > 0 {
188 remaining -= ret as usize;
189 } else if ret == 0 {
190 break;
191 } else {
192 let err = io::Error::last_os_error();
193 if err.kind() == io::ErrorKind::Interrupted {
194 continue;
195 }
196 return Err(err);
197 }
198 }
199
200 Ok(true)
201}
202
203fn head_lines_streaming_file(
208 path: &Path,
209 n: u64,
210 delimiter: u8,
211 out: &mut impl Write,
212) -> io::Result<bool> {
213 if n == 0 {
214 return Ok(true);
215 }
216
217 #[cfg(target_os = "linux")]
218 let file = {
219 use std::os::unix::fs::OpenOptionsExt;
220 std::fs::OpenOptions::new()
221 .read(true)
222 .custom_flags(libc::O_NOATIME)
223 .open(path)
224 .or_else(|_| std::fs::File::open(path))?
225 };
226 #[cfg(not(target_os = "linux"))]
227 let file = std::fs::File::open(path)?;
228
229 let mut reader = io::BufReader::with_capacity(65536, file);
230 let mut buf = [0u8; 65536];
231 let mut count = 0u64;
232
233 loop {
234 let bytes_read = match reader.read(&mut buf) {
235 Ok(0) => break,
236 Ok(n) => n,
237 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
238 Err(e) => return Err(e),
239 };
240
241 let chunk = &buf[..bytes_read];
242
243 for pos in memchr_iter(delimiter, chunk) {
244 count += 1;
245 if count == n {
246 out.write_all(&chunk[..=pos])?;
247 return Ok(true);
248 }
249 }
250
251 out.write_all(chunk)?;
252 }
253
254 Ok(true)
255}
256
257pub fn head_file(
259 filename: &str,
260 config: &HeadConfig,
261 out: &mut impl Write,
262 tool_name: &str,
263) -> io::Result<bool> {
264 let delimiter = if config.zero_terminated { b'\0' } else { b'\n' };
265
266 if filename != "-" {
267 let path = Path::new(filename);
268
269 match &config.mode {
271 HeadMode::Lines(n) => {
272 match head_lines_streaming_file(path, *n, delimiter, out) {
274 Ok(true) => return Ok(true),
275 Err(e) => {
276 eprintln!(
277 "{}: cannot open '{}' for reading: {}",
278 tool_name,
279 filename,
280 crate::common::io_error_msg(&e)
281 );
282 return Ok(false);
283 }
284 _ => {}
285 }
286 }
287 HeadMode::Bytes(n) => {
288 #[cfg(target_os = "linux")]
290 {
291 use std::os::unix::io::AsRawFd;
292 let stdout = io::stdout();
293 let out_fd = stdout.as_raw_fd();
294 if let Ok(true) = sendfile_bytes(path, *n, out_fd) {
295 return Ok(true);
296 }
297 }
298 #[cfg(not(target_os = "linux"))]
300 {
301 if let Ok(true) = head_bytes_streaming_file(path, *n, out) {
302 return Ok(true);
303 }
304 }
305 }
306 _ => {
307 }
309 }
310 }
311
312 let data: FileData = if filename == "-" {
314 match read_stdin() {
315 Ok(d) => FileData::Owned(d),
316 Err(e) => {
317 eprintln!(
318 "{}: standard input: {}",
319 tool_name,
320 crate::common::io_error_msg(&e)
321 );
322 return Ok(false);
323 }
324 }
325 } else {
326 match read_file(Path::new(filename)) {
327 Ok(d) => d,
328 Err(e) => {
329 eprintln!(
330 "{}: cannot open '{}' for reading: {}",
331 tool_name,
332 filename,
333 crate::common::io_error_msg(&e)
334 );
335 return Ok(false);
336 }
337 }
338 };
339
340 match &config.mode {
341 HeadMode::Lines(n) => head_lines(&data, *n, delimiter, out)?,
342 HeadMode::LinesFromEnd(n) => head_lines_from_end(&data, *n, delimiter, out)?,
343 HeadMode::Bytes(n) => head_bytes(&data, *n, out)?,
344 HeadMode::BytesFromEnd(n) => head_bytes_from_end(&data, *n, out)?,
345 }
346
347 Ok(true)
348}
349
350#[cfg(not(target_os = "linux"))]
352fn head_bytes_streaming_file(path: &Path, n: u64, out: &mut impl Write) -> io::Result<bool> {
353 let mut file = std::fs::File::open(path)?;
354 let mut remaining = n as usize;
355 let mut buf = [0u8; 65536];
356
357 while remaining > 0 {
358 let to_read = remaining.min(buf.len());
359 let bytes_read = match file.read(&mut buf[..to_read]) {
360 Ok(0) => break,
361 Ok(n) => n,
362 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
363 Err(e) => return Err(e),
364 };
365 out.write_all(&buf[..bytes_read])?;
366 remaining -= bytes_read;
367 }
368
369 Ok(true)
370}
371
372pub fn head_stdin_lines_streaming(n: u64, delimiter: u8, out: &mut impl Write) -> io::Result<()> {
375 if n == 0 {
376 return Ok(());
377 }
378
379 let stdin = io::stdin();
380 let mut reader = stdin.lock();
381 let mut buf = [0u8; 65536];
382 let mut count = 0u64;
383
384 loop {
385 let bytes_read = match reader.read(&mut buf) {
386 Ok(0) => break,
387 Ok(n) => n,
388 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
389 Err(e) => return Err(e),
390 };
391
392 let chunk = &buf[..bytes_read];
393
394 for pos in memchr_iter(delimiter, chunk) {
396 count += 1;
397 if count == n {
398 out.write_all(&chunk[..=pos])?;
399 return Ok(());
400 }
401 }
402
403 out.write_all(chunk)?;
405 }
406
407 Ok(())
408}