Skip to content

Commit a804ba4

Browse files
authored
Merge pull request #469 from rustcoreutils/copilot/audit-asa-utility
Audit asa utility for POSIX.2024 compliance
2 parents 2f18b7d + 372377b commit a804ba4

File tree

4 files changed

+168
-0
lines changed

4 files changed

+168
-0
lines changed

text/asa.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,33 @@ use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleC
1515
use plib::io::input_reader;
1616

1717
/// asa - interpret carriage-control characters
18+
///
19+
/// POSIX.2024 COMPLIANCE:
20+
/// - Interprets first character of each line as carriage-control character
21+
/// - Supported control characters per POSIX:
22+
/// - ' ' (space): Single spacing (advance one line)
23+
/// - '0': Double spacing (advance two lines with blank line before)
24+
/// - '1': New page (form-feed character)
25+
/// - '+': Overprint (carriage return without line advance)
26+
/// - Special rule: '+' as first character of first line treated as space
27+
/// - Extension: '-' for triple-spacing (non-POSIX)
28+
/// - Processes multiple files sequentially, each with independent state
29+
/// - Reads from stdin if no files specified or "-" given
1830
#[derive(Parser)]
1931
#[command(version, about = gettext("asa - interpret carriage-control characters"))]
2032
struct Args {
2133
/// Files to read as input.
2234
files: Vec<PathBuf>,
2335
}
2436

37+
/// Process a single file according to POSIX asa rules
38+
///
39+
/// POSIX.2024 Requirements:
40+
/// - Each line's first character is interpreted as a carriage-control character
41+
/// - The remainder of the line (after first character) is written to stdout
42+
/// - Control character determines line spacing/positioning
43+
/// - First line of file has special handling: '+' treated as space
44+
/// - Each file is processed independently with its own state
2545
fn asa_file(pathname: &PathBuf) -> io::Result<()> {
2646
let mut reader = input_reader(pathname, true)?;
2747
let mut first_line = true;

text/tests/asa/file1.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Line 1 from file1
2+
Line 2 from file1
3+
0Line 3 with double-space

text/tests/asa/file2.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
1Page 1 from file2
2+
Normal line
3+
+Overprint this

text/tests/asa/mod.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,145 @@ fn asa_cjk_control_char() {
186186
// 3-byte CJK char as control: treated as unknown, like space
187187
asa_test("日test\n", "test\n");
188188
}
189+
190+
// ===== FILE ARGUMENT TESTS =====
191+
192+
fn asa_test_with_args(args: &[&str], expected_output: &str, expected_exit_code: i32) {
193+
let str_args: Vec<String> = args.iter().map(|s| String::from(*s)).collect();
194+
run_test(TestPlan {
195+
cmd: String::from("asa"),
196+
args: str_args,
197+
stdin_data: String::new(),
198+
expected_out: String::from(expected_output),
199+
expected_err: String::from(""),
200+
expected_exit_code,
201+
});
202+
}
203+
204+
// Test single file argument
205+
#[test]
206+
fn asa_single_file() {
207+
let project_root = env!("CARGO_MANIFEST_DIR");
208+
let file1 = format!("{}/tests/asa/file1.txt", project_root);
209+
let expected = "Line 1 from file1\nLine 2 from file1\n\nLine 3 with double-space\n";
210+
asa_test_with_args(&[file1.as_str()], expected, 0);
211+
}
212+
213+
// Test multiple file arguments (files processed sequentially)
214+
#[test]
215+
fn asa_multiple_files() {
216+
let project_root = env!("CARGO_MANIFEST_DIR");
217+
let file1 = format!("{}/tests/asa/file1.txt", project_root);
218+
let file2 = format!("{}/tests/asa/file2.txt", project_root);
219+
let expected = concat!(
220+
"Line 1 from file1\n",
221+
"Line 2 from file1\n",
222+
"\n",
223+
"Line 3 with double-space\n",
224+
"\x0c",
225+
"Page 1 from file2\n",
226+
"Normal line\r",
227+
"Overprint this\n"
228+
);
229+
asa_test_with_args(&[file1.as_str(), file2.as_str()], expected, 0);
230+
}
231+
232+
// Test stdin with "-" argument
233+
#[test]
234+
fn asa_stdin_dash() {
235+
run_test(TestPlan {
236+
cmd: String::from("asa"),
237+
args: vec![String::from("-")],
238+
stdin_data: String::from(" test from stdin\n"),
239+
expected_out: String::from("test from stdin\n"),
240+
expected_err: String::from(""),
241+
expected_exit_code: 0,
242+
});
243+
}
244+
245+
// ===== EDGE CASE TESTS (POSIX COMPLIANCE) =====
246+
247+
// Test lines with only newline (control char missing)
248+
#[test]
249+
fn asa_line_with_only_newline() {
250+
// Empty line (just newline): first char is newline itself
251+
// The implementation should handle this gracefully
252+
asa_test("\n", "\n");
253+
}
254+
255+
// Test very long lines (POSIX doesn't specify line length limits for asa)
256+
#[test]
257+
fn asa_long_line() {
258+
let long_content = "x".repeat(5000);
259+
let input = format!(" {}\n", long_content);
260+
let expected = format!("{}\n", long_content);
261+
asa_test(&input, &expected);
262+
}
263+
264+
// Test tab character as control (treated as unknown, like space)
265+
#[test]
266+
fn asa_tab_control() {
267+
asa_test("\ttext\n", "text\n");
268+
}
269+
270+
// Test newline as control character (edge case)
271+
// First line is empty (just newline), second line has space control with "text"
272+
// Empty first line should output just a newline, then second line outputs normally
273+
#[test]
274+
fn asa_newline_control() {
275+
asa_test("\n text\n", "\ntext\n");
276+
}
277+
278+
// Test carriage return within content (not as control)
279+
#[test]
280+
fn asa_embedded_carriage_return() {
281+
asa_test(" text\rwith\rcr\n", "text\rwith\rcr\n");
282+
}
283+
284+
// Test form feed within content (not as control)
285+
#[test]
286+
fn asa_embedded_form_feed() {
287+
asa_test(" text\x0cwith\x0cff\n", "text\x0cwith\x0cff\n");
288+
}
289+
290+
// ===== SEQUENTIAL FILE PROCESSING =====
291+
292+
// Test that each file starts as if it's independent
293+
// (first line of second file should be treated as first line)
294+
#[test]
295+
fn asa_file_independence() {
296+
let project_root = env!("CARGO_MANIFEST_DIR");
297+
let file2 = format!("{}/tests/asa/file2.txt", project_root);
298+
// file2.txt starts with '1' (form-feed) as first line
299+
// Should output form-feed at start since it's the first line of the file
300+
let expected = "\x0cPage 1 from file2\nNormal line\rOverprint this\n";
301+
asa_test_with_args(&[file2.as_str()], expected, 0);
302+
}
303+
304+
// ===== SPECIAL CHARACTER COMBINATIONS =====
305+
306+
// Test all standard control characters in sequence
307+
#[test]
308+
fn asa_all_controls_sequence() {
309+
let input = " space\n0zero\n1one\n+plus\n";
310+
let expected = "space\n\nzero\n\x0cone\rplus\n";
311+
asa_test(input, expected);
312+
}
313+
314+
// Test repeated form-feeds
315+
#[test]
316+
fn asa_multiple_form_feeds() {
317+
asa_test(
318+
"1page1\n1page2\n1page3\n",
319+
"\x0cpage1\n\x0cpage2\n\x0cpage3\n",
320+
);
321+
}
322+
323+
// Test alternating double-space and normal
324+
#[test]
325+
fn asa_alternating_spacing() {
326+
asa_test(
327+
" normal\n0double\n normal\n0double\n",
328+
"normal\n\ndouble\nnormal\n\ndouble\n",
329+
);
330+
}

0 commit comments

Comments
 (0)