1use std::io::{self, Read, Write};
2use std::path::Path;
3
4use crate::common::io::{read_file, read_stdin};
5
6#[derive(Clone, Debug, Default)]
8pub struct CatConfig {
9 pub number: bool,
10 pub number_nonblank: bool,
11 pub show_ends: bool,
12 pub show_tabs: bool,
13 pub show_nonprinting: bool,
14 pub squeeze_blank: bool,
15}
16
17impl CatConfig {
18 pub fn is_plain(&self) -> bool {
20 !self.number
21 && !self.number_nonblank
22 && !self.show_ends
23 && !self.show_tabs
24 && !self.show_nonprinting
25 && !self.squeeze_blank
26 }
27}
28
29#[cfg(target_os = "linux")]
31pub fn splice_file_to_stdout(path: &Path) -> io::Result<bool> {
32 use std::os::unix::fs::OpenOptionsExt;
33 use std::os::unix::io::AsRawFd;
34
35 let stdout = io::stdout();
37 let out_fd = stdout.as_raw_fd();
38 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
39 if unsafe { libc::fstat(out_fd, &mut stat) } != 0 {
40 return Ok(false);
41 }
42 let stdout_is_pipe = (stat.st_mode & libc::S_IFMT) == libc::S_IFIFO;
43
44 let file = std::fs::OpenOptions::new()
45 .read(true)
46 .custom_flags(libc::O_NOATIME)
47 .open(path)
48 .or_else(|_| std::fs::File::open(path))?;
49
50 let in_fd = file.as_raw_fd();
51 let metadata = file.metadata()?;
52 let file_size = metadata.len() as usize;
53
54 if file_size == 0 {
55 return Ok(true);
56 }
57
58 if stdout_is_pipe {
59 let mut remaining = file_size;
61 while remaining > 0 {
62 let chunk = remaining.min(1024 * 1024 * 1024);
63 let ret = unsafe {
64 libc::splice(
65 in_fd,
66 std::ptr::null_mut(),
67 out_fd,
68 std::ptr::null_mut(),
69 chunk,
70 libc::SPLICE_F_MOVE,
71 )
72 };
73 if ret > 0 {
74 remaining -= ret as usize;
75 } else if ret == 0 {
76 break;
77 } else {
78 let err = io::Error::last_os_error();
79 if err.kind() == io::ErrorKind::Interrupted {
80 continue;
81 }
82 return sendfile_to_stdout(in_fd, file_size, out_fd);
84 }
85 }
86 Ok(true)
87 } else {
88 sendfile_to_stdout(in_fd, file_size, out_fd)
90 }
91}
92
93#[cfg(target_os = "linux")]
94fn sendfile_to_stdout(in_fd: i32, file_size: usize, out_fd: i32) -> io::Result<bool> {
95 let mut offset: libc::off_t = 0;
96 let mut remaining = file_size;
97
98 while remaining > 0 {
99 let chunk = remaining.min(0x7ffff000);
100 let ret = unsafe { libc::sendfile(out_fd, in_fd, &mut offset, chunk) };
101 if ret > 0 {
102 remaining -= ret as usize;
103 } else if ret == 0 {
104 break;
105 } else {
106 let err = io::Error::last_os_error();
107 if err.kind() == io::ErrorKind::Interrupted {
108 continue;
109 }
110 return Err(err);
111 }
112 }
113
114 Ok(true)
115}
116
117pub fn cat_plain_file(path: &Path, out: &mut impl Write) -> io::Result<bool> {
119 #[cfg(target_os = "linux")]
121 {
122 match splice_file_to_stdout(path) {
123 Ok(true) => return Ok(true),
124 Ok(false) => {}
125 Err(_) => {} }
127 }
128
129 let data = read_file(path)?;
131 if !data.is_empty() {
132 out.write_all(&data)?;
133 }
134 Ok(true)
135}
136
137pub fn cat_plain_stdin(out: &mut impl Write) -> io::Result<()> {
139 #[cfg(target_os = "linux")]
140 {
141 let stdin_fd = 0i32;
143 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
144 if unsafe { libc::fstat(1, &mut stat) } == 0
145 && (stat.st_mode & libc::S_IFMT) == libc::S_IFIFO
146 {
147 loop {
149 let ret = unsafe {
150 libc::splice(
151 stdin_fd,
152 std::ptr::null_mut(),
153 1,
154 std::ptr::null_mut(),
155 1024 * 1024 * 1024,
156 libc::SPLICE_F_MOVE,
157 )
158 };
159 if ret > 0 {
160 continue;
161 } else if ret == 0 {
162 return Ok(());
163 } else {
164 let err = io::Error::last_os_error();
165 if err.kind() == io::ErrorKind::Interrupted {
166 continue;
167 }
168 break;
170 }
171 }
172 }
173 }
174
175 let stdin = io::stdin();
177 let mut reader = stdin.lock();
178 let mut buf = [0u8; 131072]; loop {
180 let n = match reader.read(&mut buf) {
181 Ok(0) => break,
182 Ok(n) => n,
183 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
184 Err(e) => return Err(e),
185 };
186 out.write_all(&buf[..n])?;
187 }
188 Ok(())
189}
190
191fn _build_nonprinting_table(show_tabs: bool) -> ([u8; 256], [bool; 256]) {
195 let mut table = [0u8; 256];
196 let mut multi = [false; 256];
197
198 for i in 0..256u16 {
199 let b = i as u8;
200 match b {
201 b'\n' => {
202 table[i as usize] = b'\n';
203 }
204 b'\t' => {
205 if show_tabs {
206 table[i as usize] = b'I';
207 multi[i as usize] = true;
208 } else {
209 table[i as usize] = b'\t';
210 }
211 }
212 0..=8 | 10..=31 => {
213 table[i as usize] = b + 64;
215 multi[i as usize] = true;
216 }
217 32..=126 => {
218 table[i as usize] = b;
219 }
220 127 => {
221 table[i as usize] = b'?';
223 multi[i as usize] = true;
224 }
225 128..=159 => {
226 table[i as usize] = b - 128 + 64;
228 multi[i as usize] = true;
229 }
230 160..=254 => {
231 table[i as usize] = b - 128;
233 multi[i as usize] = true;
234 }
235 255 => {
236 table[i as usize] = b'?';
238 multi[i as usize] = true;
239 }
240 }
241 }
242
243 (table, multi)
244}
245
246#[inline]
248fn write_nonprinting(b: u8, show_tabs: bool, out: &mut Vec<u8>) {
249 match b {
250 b'\t' if !show_tabs => out.push(b'\t'),
251 b'\n' => out.push(b'\n'),
252 0..=8 | 10..=31 => {
253 out.push(b'^');
254 out.push(b + 64);
255 }
256 9 => {
257 out.push(b'^');
259 out.push(b'I');
260 }
261 32..=126 => out.push(b),
262 127 => {
263 out.push(b'^');
264 out.push(b'?');
265 }
266 128..=159 => {
267 out.push(b'M');
268 out.push(b'-');
269 out.push(b'^');
270 out.push(b - 128 + 64);
271 }
272 160..=254 => {
273 out.push(b'M');
274 out.push(b'-');
275 out.push(b - 128);
276 }
277 255 => {
278 out.push(b'M');
279 out.push(b'-');
280 out.push(b'^');
281 out.push(b'?');
282 }
283 }
284}
285
286pub fn cat_with_options(
288 data: &[u8],
289 config: &CatConfig,
290 line_num: &mut u64,
291 out: &mut impl Write,
292) -> io::Result<()> {
293 if data.is_empty() {
294 return Ok(());
295 }
296
297 let estimated = data.len() + data.len() / 10 + 1024;
300 let mut buf = Vec::with_capacity(estimated.min(16 * 1024 * 1024));
301
302 let mut prev_blank = false;
303 let mut pos = 0;
304 let mut itoa_buf = itoa::Buffer::new();
305
306 while pos < data.len() {
307 let line_end = memchr::memchr(b'\n', &data[pos..])
309 .map(|p| pos + p + 1)
310 .unwrap_or(data.len());
311
312 let line = &data[pos..line_end];
313 let is_blank = line == b"\n" || line.is_empty();
314
315 if config.squeeze_blank && is_blank && prev_blank {
317 pos = line_end;
318 continue;
319 }
320 prev_blank = is_blank;
321
322 if config.number_nonblank {
324 if !is_blank {
325 let s = itoa_buf.format(*line_num);
326 let pad = if s.len() < 6 { 6 - s.len() } else { 0 };
328 buf.extend(std::iter::repeat_n(b' ', pad));
329 buf.extend_from_slice(s.as_bytes());
330 buf.push(b'\t');
331 *line_num += 1;
332 }
333 } else if config.number {
334 let s = itoa_buf.format(*line_num);
335 let pad = if s.len() < 6 { 6 - s.len() } else { 0 };
336 buf.extend(std::iter::repeat_n(b' ', pad));
337 buf.extend_from_slice(s.as_bytes());
338 buf.push(b'\t');
339 *line_num += 1;
340 }
341
342 if config.show_nonprinting || config.show_tabs {
344 let content_end = if line.last() == Some(&b'\n') {
345 line.len() - 1
346 } else {
347 line.len()
348 };
349
350 for &b in &line[..content_end] {
351 if config.show_nonprinting {
352 write_nonprinting(b, config.show_tabs, &mut buf);
353 } else if config.show_tabs && b == b'\t' {
354 buf.extend_from_slice(b"^I");
355 } else {
356 buf.push(b);
357 }
358 }
359
360 if config.show_ends && line.last() == Some(&b'\n') {
361 buf.push(b'$');
362 }
363 if line.last() == Some(&b'\n') {
364 buf.push(b'\n');
365 }
366 } else {
367 if config.show_ends {
369 let content_end = if line.last() == Some(&b'\n') {
370 line.len() - 1
371 } else {
372 line.len()
373 };
374 buf.extend_from_slice(&line[..content_end]);
375 if line.last() == Some(&b'\n') {
376 buf.push(b'$');
377 buf.push(b'\n');
378 }
379 } else {
380 buf.extend_from_slice(line);
381 }
382 }
383
384 if buf.len() >= 8 * 1024 * 1024 {
386 out.write_all(&buf)?;
387 buf.clear();
388 }
389
390 pos = line_end;
391 }
392
393 if !buf.is_empty() {
394 out.write_all(&buf)?;
395 }
396
397 Ok(())
398}
399
400pub fn cat_file(
402 filename: &str,
403 config: &CatConfig,
404 line_num: &mut u64,
405 out: &mut impl Write,
406 tool_name: &str,
407) -> io::Result<bool> {
408 if filename == "-" {
409 if config.is_plain() {
410 match cat_plain_stdin(out) {
411 Ok(()) => return Ok(true),
412 Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
413 std::process::exit(0);
414 }
415 Err(e) => {
416 eprintln!(
417 "{}: standard input: {}",
418 tool_name,
419 crate::common::io_error_msg(&e)
420 );
421 return Ok(false);
422 }
423 }
424 }
425 match read_stdin() {
426 Ok(data) => {
427 cat_with_options(&data, config, line_num, out)?;
428 Ok(true)
429 }
430 Err(e) => {
431 eprintln!(
432 "{}: standard input: {}",
433 tool_name,
434 crate::common::io_error_msg(&e)
435 );
436 Ok(false)
437 }
438 }
439 } else {
440 let path = Path::new(filename);
441
442 match std::fs::metadata(path) {
444 Ok(meta) if meta.is_dir() => {
445 eprintln!("{}: {}: Is a directory", tool_name, filename);
446 return Ok(false);
447 }
448 _ => {}
449 }
450
451 if config.is_plain() {
452 match cat_plain_file(path, out) {
453 Ok(true) => return Ok(true),
454 Ok(false) => {} Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
456 std::process::exit(0);
457 }
458 Err(e) => {
459 eprintln!(
460 "{}: {}: {}",
461 tool_name,
462 filename,
463 crate::common::io_error_msg(&e)
464 );
465 return Ok(false);
466 }
467 }
468 }
469
470 match read_file(path) {
471 Ok(data) => {
472 cat_with_options(&data, config, line_num, out)?;
473 Ok(true)
474 }
475 Err(e) => {
476 eprintln!(
477 "{}: {}: {}",
478 tool_name,
479 filename,
480 crate::common::io_error_msg(&e)
481 );
482 Ok(false)
483 }
484 }
485 }
486}