Author SHA1 Message Date
Sylvestre Ledru 2ce633cb8d prepare version 0.2.0 (#519) 2026-08-09 01:57:25 +02:00
Mukunda Rao Katta 91465e566b fix: identify uutils in cli output (#435) 2026-08-09 00:55:37 +02:00
Sylvestre Ledru d842aa305e sed: fix clippy cast_possible_truncation errors (#518) 2026-08-09 00:52:26 +02:00
xtqqczze d28a1f8e73 chore(cargo): remove deprecated package authors field (#444) 2026-08-08 23:28:22 +02:00
renovate[bot] af5f8afdcb Update Rust crate clap_complete to v4.6.9 2026-08-07 08:07:56 +02:00
renovate[bot] 8f3f126816 Update Rust crate clap_mangen to v0.3.2 2026-08-07 08:05:08 +02:00
renovate[bot] c771318a7a Update Rust crate clap to v4.6.6 2026-08-07 08:03:52 +02:00
renovate[bot] 36a1cf4954 Update Rust crate uucore to 0.10.0 2026-08-06 09:02:46 +02:00
renovate[bot] 13bb82ba13 Update Rust crate clap_mangen to v0.3.1 2026-08-06 08:32:10 +02:00
renovate[bot] cb9eb1da42 Update Rust crate uufuzz to 0.10.0 2026-08-06 08:15:55 +02:00
renovate[bot] b101462d1b Update Rust crate uutests to 0.10.0 2026-08-06 08:10:31 +02:00
oech3 9f2e1a1ad4 remove sysinfo dep 2026-08-05 14:18:30 +02:00
Franklin-Qi 2b092d53b0 fast_io.rs: replace the unsafe libc::copy_file_range call by rustix (#458)
Replace the unsafe `libc::copy_file_range` call with the safe `rustix::fs::copy_file_range`.

Closes: #442
2026-08-05 13:32:45 +02:00
renovate[bot] 550d545d72 Update Rust crate similar to v3.1.2 2026-08-05 08:01:27 +02:00
Diomidis Spinellisandmukunda katta a5d219764f feat: add T command (branch on no-substitution) (#508)
* feat: add T command (branch on no-substitution)

Implements the GNU `T LABEL` command: branch to LABEL if no s///
substitution has succeeded since the last input line was read or the
last t/T was taken. It is the inverse of `t` and shares the same
address and label parsing.

Wires 'T' into get_cmd_spec, resolve_branch_targets, and the processor
branch logic, resetting the substitution flag on evaluation just like
't'. Adds integration tests (simple branch + flag-clear) and a cmd-spec
unit test.

Closes #395

Co-authored-by: Diomidis Spinellis <dds@aueb.gr>

* ci: re-trigger checks (windows runner crates.io download flake)

* Disable T in POSIX mode

---------

Co-authored-by: mukunda katta <mukunda.vjcs6@gmail.com>
2026-08-02 17:47:18 +03:00
Develandpre-commit-ci[bot] 397ab86302 Implement v command #397 (#420)
* implement version command

* add tests for version option

* fix patch comparsion

* absurd extreme comparsion fix

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* accept missing minor or patch

* guard 'v' with if posix

* add a few tests

* add test_default_version

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* check if minor is empty and push zero instead of shadowing

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-01 18:18:05 +03:00
Mukunda Rao Katta 2288789f33 fix: allow closing brace as command terminator (#427) 2026-08-01 14:03:34 +03:00
Pranav Ram Joshi 84ae1b07a7 Implement e (execute) command (#481)
* chore: fix typo

* add: standalone e command

Implement the 'e' command. No argument implies execution of pattern space and any respective argument is to run with the output written to stream. Rejected under --posix and --sandbox.

Also stop silently swallowing shell stderr for e and s///e.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* add: e command tests

Includes 17 new tests. Cover both forms of the new 'e' command: with-argument execution (including shell side effects, addressing, ';' not separating the argument, escape decoding, and backslash continuation) and the no-argument pattern-space form. Also cover --posix/--sandbox rejection and stderr surfacing on command failure.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* chore: gate LF-related tests to UNIIX only

* address e command review

Validate shell output as utf-8 and error out instead of lossily converting it. Make CRLF strip conditionally compile to Windows only. Rename execute_pattern_as_shell to execute_pattern_as_shell_command. Add tests for non-utf-8 error paths.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* add: Windows tests for e command with argument

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* fix: broken pipe race in sandbox e command test

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* Apply s/// p and e flags in written order

GNU sed applies the 'p' and 'e' substitution flags in the order they appear. 'pe' prints the pre-execution text then executes, while 'ep' executes then prints the result. Record whether 'p' precedes 'e' and apply them accordingly.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

---------

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>
2026-08-01 13:29:42 +03:00
renovate[bot] f32296fc22 Update Rust crate clap to v4.6.5 2026-08-01 07:12:40 +02:00
renovate[bot] 7239fb0e08 Update Rust crate ctor to v1.0.12 2026-07-30 07:57:26 +02:00
renovate[bot] c7e1f78486 Update mozilla-actions/sccache-action action to v0.0.11 2026-07-29 15:19:54 +02:00
renovate[bot] fb907703a0 Update CodSpeedHQ/action action to v5 2026-07-29 14:01:03 +02:00
renovate[bot] 3d52077d7f Update Rust crate clap_complete to v4.6.8 2026-07-27 17:37:39 +02:00
Diomidis Spinellis e9792a8ef9 Improve GNU compatibility through byte processing (#487)
The original Rust implementation assummed a UTF-8 environment, as is
common in modern systems.  This commit improves GNU sed compatibility
by changing most processing to run on bytes rather than Rust's Unicode
characters and strings. This change also improves the handling of binary
files. The commit adds a simple POSIX-compatible locale configuration
to support UTF-8 processing when specified. While at it, also fix
script-specified file handling to take place through OsString and
add a few missing unit tests.

The change fixes two failing GNU test suite cases.
2026-07-26 19:44:54 +02:00
renovate[bot] c732b60cfc Update Rust crate ctor to v1.0.11 (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-26 19:43:52 +02:00
renovate[bot] c1e5d8ea08 Update Rust crate ctor to v1.0.10 2026-07-22 08:23:11 +02:00
renovate[bot] 6887201cce Update Rust crate clap to v4.6.4 2026-07-22 08:09:28 +02:00
renovate[bot] 75d856e4c2 Update Rust crate libc to v0.2.189 2026-07-22 08:00:30 +02:00
renovate[bot] d7a2106258 Update Rust crate libc to v0.2.188 2026-07-21 14:00:30 +02:00
renovate[bot] a726ac6959 Update Rust crate clap to v4.6.3 2026-07-21 08:13:36 +02:00
renovate[bot] 49590e63be Update Rust crate libc to v0.2.187 2026-07-21 08:03:39 +02:00
renovate[bot] 99a9957e89 Update Rust crate ctor to v1.0.9 2026-07-16 08:48:11 +02:00
renovate[bot] c521271ca8 Update Rust crate regex to v1.13.1 2026-07-16 08:19:19 +02:00
renovate[bot] 7830c20bc1 Update Rust crate clap to v4.6.2 2026-07-16 08:08:20 +02:00
renovate[bot] b37e23fa98 Update Rust crate regex to v1.13.0 2026-07-10 10:55:46 +02:00
Daniel Hofstetter 07eada8531 Bump crossbeam-epoch from 0.9.18 to 0.9.20 (#486)
https://rustsec.org/advisories/RUSTSEC-2026-0204
2026-07-08 18:57:10 +03:00
renovate[bot] 15def35a2a chore(deps): update rust crate memchr to v2.8.3 2026-07-08 07:50:55 +02:00
21 changed files with 2742 additions and 1165 deletions

No files matched your search

+2 -2
View File
@@ -33,7 +33,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.11
- name: Install tools
uses: taiki-e/install-action@v2
@@ -47,7 +47,7 @@ jobs:
cargo codspeed build -p ${{ matrix.benchmark-target.package }}
- name: Run benchmarks for ${{ matrix.benchmark-target.package }}
uses: CodSpeedHQ/action@v4
uses: CodSpeedHQ/action@v5
env:
CODSPEED_LOG: debug
with:
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.11
- name: Initialize workflow variables
id: vars
shell: bash
Generated
+78 -339
View File
File diff suppressed because it is too large. Load diff
+6 -8
View File
@@ -5,8 +5,7 @@
[package]
name = "sed"
version = "0.1.1"
authors = ["uutils developers"]
version = "0.2.0"
license = "MIT"
description = "sed ~ implemented as universal (cross-platform) utils, written in Rust"
default-run = "sed"
@@ -29,7 +28,6 @@ sed = []
[workspace.dependencies]
assert_fs = "1.1.3"
bytesize = "2.0.0"
chrono = { version = "0.4.37", default-features = false, features = [
"clock",
] }
@@ -48,11 +46,11 @@ predicates = "3.1.3"
rand = { version = "0.10.0" }
regex = "1.10.4"
sha2 = "0.11"
sysinfo = "0.38"
tempfile = "3.10.1"
terminal_size = "0.4.2"
textwrap = { version = "0.16.1", features = ["terminal_size"] }
uucore = { version = "0.9.0", features = ["libc"] }
uucore = { version = "0.10.0", features = ["libc"] }
rustix = "1.1.4"
xattr = "1.3.1"
@@ -66,11 +64,11 @@ memmap2.workspace = true
phf = { workspace = true }
predicates = { workspace = true }
regex = { workspace = true }
sysinfo = { workspace = true }
tempfile = { workspace = true }
terminal_size = { workspace = true }
textwrap = { workspace = true }
uucore = { workspace = true }
rustix = { workspace = true }
[dev-dependencies]
assert_fs = { workspace = true }
@@ -85,7 +83,7 @@ regex = { workspace = true }
sha2 = { workspace = true }
tempfile = { workspace = true }
uucore = { workspace = true, features = ["entries", "process", "signals", "benchmark"] }
uutests = "0.9.0"
uutests = "0.10.0"
[target.'cfg(unix)'.dev-dependencies]
xattr = { workspace = true }
@@ -135,7 +133,7 @@ must_use_candidate = "allow" # 56
needless_raw_string_hashes = "allow" # 20
needless_pass_by_value = "allow" # 15
missing_panics_doc = "allow" # 12
cast_possible_truncation = "allow" # 7
# cast_possible_truncation = "allow" # 7
unnecessary_wraps = "allow" # 6
match_wildcard_for_single_variants = "allow" # 4
cast_sign_loss = "allow" # 4
+13 -6
View File
@@ -116,12 +116,19 @@ cargo test
sequences.
### Incompatibilities
* The input is assumed to be valid UTF-8 (this includes 7-bit ASCII).
If the input is in another code page, consider converting it through UTF-8
in order to avoid errors on invalid UTF-8 sequences and for the correct
handling of regular expressions.
This _sed_ program can also handle arbitrary byte sequences if no part of the
input is treated as string.
* Similarly to GNU _sed_, input is processed as raw bytes or as valid UTF-8
(this includes 7-bit ASCII) based on the locale as specified by the
`LC_ALL`, `LC_CTYPE`, and `LANG` environment variables,
with the default being byte processing.
However, in contrast with GNU _sed_, other locales (e.g. ISO-8859-1)
are not supported. If the input is in another code page or encoding
and requires locale-specific processing (e.g. ignore/map case,
character classes), consider converting it through UTF-8 to ensure
the correct handling of locale-specific regular expressions.
This _sed_ program can also handle arbitrary byte sequences
if no part of the input requires treating it as a Rust String.
* Back-references aren't supported when input is processed as bytes
(`LC_ALL=C`).
* The command will report an error and fail if duplicate labels are found
in the script.
This matches the BSD behavior. The GNU version accepts duplicate labels.
+13 -127
View File
@@ -571,9 +571,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.186"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libfuzzer-sys"
@@ -646,15 +646,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "ntapi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -683,25 +674,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -937,7 +909,7 @@ dependencies = [
"phf_codegen",
"predicates",
"regex",
"sysinfo",
"rustix",
"tempfile",
"terminal_size",
"textwrap",
@@ -1013,9 +985,9 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "similar"
version = "3.1.1"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16"
checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6"
dependencies = [
"bstr",
]
@@ -1055,20 +1027,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sysinfo"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -1076,7 +1034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys",
@@ -1194,9 +1152,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uucore"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069b34217c27f611e1589f540f58118dbf226a9e407d38ab472052ff075a1dc2"
checksum = "e97dd0fd8bb2ed12affba9f956c3b94b8ad3f9432746717475420e916ae0b8d4"
dependencies = [
"bigdecimal",
"clap",
@@ -1212,6 +1170,7 @@ dependencies = [
"rustix",
"thiserror",
"unic-langid",
"unicode-width",
"uucore_procs",
"wild",
]
@@ -1233,9 +1192,9 @@ dependencies = [
[[package]]
name = "uucore_procs"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34337da211e7abfff7189b794afb3b5018fe356fb36474b73645458fc1201350"
checksum = "907f1b828046ea42cdb4bd898f05b48ab873a41937bf75ebe12f09a5bf411f58"
dependencies = [
"proc-macro2",
"quote",
@@ -1243,9 +1202,9 @@ dependencies = [
[[package]]
name = "uufuzz"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e4192b67d5e7f0a3300db353ec3217beee89442f02c11d8ae108867eee392d0"
checksum = "72d4c5bac0f8ccabaa8e8ccbf2724446f8b007c06ad343fabfe1a1525a4ff4f2"
dependencies = [
"console",
"rand",
@@ -1361,49 +1320,6 @@ dependencies = [
"glob",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -1417,17 +1333,6 @@ dependencies = [
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
@@ -1456,16 +1361,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
@@ -1493,15 +1388,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
+2 -2
View File
@@ -14,8 +14,8 @@ libc = "0.2.153"
tempfile = "3.15.0"
rand = { version = "0.10.0" }
similar = "3.0.0"
uucore = { version = "0.9.0", features = ["libc"] }
uufuzz = "0.9.0"
uucore = { version = "0.10.0", features = ["libc"] }
uufuzz = "0.10.0"
sed = { path = ".." }
+152 -78
View File
@@ -52,6 +52,8 @@ pub struct ProcessingContext {
pub last_file: bool,
/// Stop processing further input.
pub stop_processing: bool,
/// Whether sed operates on bytes or UTF-8 characters
pub character_mode: CharacterMode,
/// Previously compiled RE, saved for reuse when specifying an empty RE
pub saved_regex: Option<Regex>,
/// Modification of input processing action
@@ -59,7 +61,7 @@ pub struct ProcessingContext {
// command.
pub input_action: Option<InputAction>,
/// Hold space
pub hold: StringSpace,
pub hold: ByteSpace,
/// Nesting of { } at compile time
pub parsed_block_nesting: usize,
/// Command associated with each label
@@ -75,14 +77,22 @@ pub struct ProcessingContext {
#[derive(Clone, Debug)]
/// Elements that shall be appended at the end of each command processing cycle
pub enum AppendElement {
Text(Rc<str>), // The specified text string
Path(PathBuf), // The contents of the specified file path
Text(Rc<[u8]>), // The specified text bytes
Path(PathBuf), // The contents of the specified file path
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
/// Whether sed operates on bytes or UTF-8 characters
pub enum CharacterMode {
Byte, // Interpret data as arbitrary bytes (C/POSIX locale).
#[default]
Utf8, // Interpret data as UTF-8 characters (UTF-8 locale).
}
#[derive(Clone, Debug, Default, PartialEq)]
/// A space mirroring IOChunk, but only with a String
pub struct StringSpace {
pub content: String, // Line content without newline
/// A space mirroring IOChunk without mmap-backed storage.
pub struct ByteSpace {
pub content: Vec<u8>, // Line content without newline
pub has_newline: bool, // True if \n-terminated
}
@@ -100,9 +110,9 @@ pub enum Address {
#[derive(Debug)]
/// A single part of an RE replacement
pub enum ReplacementPart {
Literal(String), // Normal text
WholeMatch, // &
Group(u32), // \1 to \9
Literal(Vec<u8>), // Normal text
WholeMatch, // &
Group(u32), // \1 to \9
}
// The maximum value allowed in regex quantifier
@@ -152,8 +162,8 @@ impl ReplacementTemplate {
/// let result = regex.replace_all(input, |caps: &Captures| {
/// template.apply_captures(&command, caps) });
/// Returns an error if a backreference in the template was not matched by the RE.
pub fn apply_captures(&self, command: &Command, caps: &Captures) -> UResult<String> {
let mut result = String::new();
pub fn apply_captures(&self, command: &Command, caps: &Captures) -> UResult<Vec<u8>> {
let mut result = Vec::new();
// Invalid group numbers may end here through (unkown at compile time)
// reused REs.
@@ -169,15 +179,17 @@ impl ReplacementTemplate {
for part in &self.parts {
match part {
ReplacementPart::Literal(s) => result.push_str(s),
ReplacementPart::Literal(s) => result.extend_from_slice(s),
ReplacementPart::WholeMatch => {
result.push_str(caps.get(0)?.map(|m| m.as_str()).unwrap_or_default());
result
.extend_from_slice(caps.get(0)?.map(|m| m.as_bytes()).unwrap_or_default());
}
ReplacementPart::Group(n) => {
let i: usize = (*n).try_into().unwrap();
result.push_str(caps.get(i)?.map(|m| m.as_str()).unwrap_or_default());
result
.extend_from_slice(caps.get(i)?.map(|m| m.as_bytes()).unwrap_or_default());
}
}
}
@@ -186,14 +198,14 @@ impl ReplacementTemplate {
}
/// Apply the template to the given RE single match.
pub fn apply_match(&self, m: &Match) -> String {
let mut result = String::new();
pub fn apply_match(&self, m: &Match) -> Vec<u8> {
let mut result = Vec::new();
for part in &self.parts {
match part {
ReplacementPart::Literal(s) => result.push_str(s),
ReplacementPart::Literal(s) => result.extend_from_slice(s),
ReplacementPart::WholeMatch => result.push_str(m.as_str()),
ReplacementPart::WholeMatch => result.extend_from_slice(m.as_bytes()),
ReplacementPart::Group(_) => {
panic!("unexpected Regex group replacement")
@@ -211,12 +223,20 @@ pub struct Substitution {
pub replacement: ReplacementTemplate, // Specified broken-down replacement
pub occurrence: usize, // Which occurrence to substitute
pub print_flag: bool, // True if 'p' flag
pub p_before_e: bool, // True if 'p' appears before 'e'
pub ignore_case: bool, // True if 'I' flag
pub execute: bool, // True if 'e' flag (GNU extension)
pub multiline: bool, // True if 'm' or 'M' flag (GNU extension)
pub write_file: Option<Rc<RefCell<NamedWriter>>>, // Writer to file if 'w' flag is used
}
#[derive(Debug, PartialEq, Eq)]
/// Result of parsing a transliteration string in byte or character mode.
pub enum ParsedTransliteration {
Bytes(Vec<u8>),
Text(String),
}
/// The block of the first and most common Unicode characters:
/// ASCII, Latin Extended, Greek, Curillic, Coptic, Arabic, etc.
/// It comprises all UCS-2 characters. We use a fast lookup array for these.
@@ -225,20 +245,28 @@ const COMMON_UNICODE: usize = 2048;
#[derive(Debug)]
/// Transliteration command (y)
pub struct Transliteration {
fast: [char; COMMON_UNICODE],
slow: HashMap<char, char>,
byte_fast: [u8; 256],
unicode_fast: [char; COMMON_UNICODE],
unicode_slow: HashMap<char, char>,
pub(crate) is_byte_identity: bool,
}
impl Default for Transliteration {
/// Create a new Transliteration with identity mapping for the fast-path.
fn default() -> Self {
let mut fast = ['\0'; COMMON_UNICODE];
for (i, slot) in fast.iter_mut().enumerate() {
*slot = char::from_u32(i as u32).unwrap_or('\0');
let mut fast = [0u8; 256];
for (slot, value) in fast.iter_mut().zip(0..=u8::MAX) {
*slot = value;
}
let mut unicode_fast = ['\0'; COMMON_UNICODE];
for (slot, cp) in unicode_fast.iter_mut().zip(0u32..) {
*slot = char::from_u32(cp).unwrap_or('\0');
}
Self {
fast,
slow: HashMap::new(),
byte_fast: fast,
unicode_fast,
unicode_slow: HashMap::new(),
is_byte_identity: true,
}
}
}
@@ -253,25 +281,44 @@ impl Transliteration {
result
}
/// Create through byte mappings from `source` to `target`.
pub fn from_bytes(source: &[u8], target: &[u8]) -> Self {
let mut result = Self::default();
for (&from, &to) in source.iter().zip(target) {
result.byte_fast[from as usize] = to;
}
result
}
/// Set a transliteration mapping from one character to another.
fn insert(&mut self, from: char, to: char) {
let cp = from as usize;
if cp < COMMON_UNICODE {
self.fast[cp] = to;
self.unicode_fast[cp] = to;
} else {
self.slow.insert(from, to);
self.unicode_slow.insert(from, to);
}
if from.is_ascii() && to.is_ascii() {
self.byte_fast[from as usize] = to as u8;
} else {
self.is_byte_identity = false;
}
}
/// Look up a character transliteration.
pub fn lookup(&self, ch: char) -> char {
pub fn lookup_char(&self, ch: char) -> char {
let cp = ch as usize;
if cp < COMMON_UNICODE {
self.fast[cp]
self.unicode_fast[cp]
} else {
self.slow.get(&ch).copied().unwrap_or(ch)
self.unicode_slow.get(&ch).copied().unwrap_or(ch)
}
}
/// Fast byte lookup for pure ASCII transliterations.
pub fn lookup_byte(&self, byte: u8) -> u8 {
self.byte_fast[byte as usize]
}
}
#[derive(Debug)]
@@ -314,16 +361,16 @@ impl Command {
#[derive(Debug)]
/// Command-specific data
/// After parsing, t, b Label elements are converted into BranchTarget ones.
/// After parsing, t, T, b Label elements are converted into BranchTarget ones.
pub enum CommandData {
None,
BranchTarget(Option<Rc<RefCell<Command>>>), // Commands for 'b', 't', '{'
Label(Option<String>), // Label name for 'b', 't', ':'
BranchTarget(Option<Rc<RefCell<Command>>>), // Commands for 'b', 't', 'T', '{'
Label(Option<String>), // Label name for 'b', 't', 'T', ':'
Path(PathBuf), // File path for 'r'
NamedWriter(Rc<RefCell<NamedWriter>>), // File output for 'w'
Number(usize), // Number for 'l', 'q', 'Q' (GNU)
Substitution(Box<Substitution>), // Substitute command 's'
Text(Rc<str>), // Text for 'a', 'c', 'i'
Text(Rc<[u8]>), // Text for 'a', 'c', 'i'
Transliteration(Box<Transliteration>), // Transliteration command 'y'
}
@@ -340,7 +387,7 @@ pub struct InputAction {
/// Next command to execute (rather than commands from start)
pub next_command: Option<Rc<RefCell<Command>>>,
/// Data to prepend to the read contents
pub prepend: String,
pub prepend: Vec<u8>,
}
#[cfg(test)]
@@ -350,7 +397,7 @@ mod tests {
// Return the captures for the RE applied to the specified string
fn caps_for<'a>(re: &str, chunk: &'a mut IOChunk) -> Captures<'a> {
Regex::new(re)
Regex::new(re, CharacterMode::Utf8)
.unwrap()
.captures(chunk)
.unwrap()
@@ -366,26 +413,26 @@ mod tests {
let cmd = Command::default();
let result = template.apply_captures(&cmd, &caps).unwrap();
assert_eq!(result, "");
assert_eq!(result, b"");
}
#[test]
// s/abc/hello/
fn test_literal_only() {
let template = ReplacementTemplate::new(vec![ReplacementPart::Literal("hello".into())]);
let template = ReplacementTemplate::new(vec![ReplacementPart::Literal(b"hello".to_vec())]);
let input = &mut IOChunk::new_from_str("abc");
let caps = caps_for("abc", input);
let cmd = Command::default();
let result = template.apply_captures(&cmd, &caps).unwrap();
assert_eq!(result, "hello");
assert_eq!(result, b"hello");
}
#[test]
// s/foo\d+/got: &/
fn test_whole_match() {
let template = ReplacementTemplate::new(vec![
ReplacementPart::Literal("got: ".into()),
ReplacementPart::Literal(b"got: ".to_vec()),
ReplacementPart::WholeMatch,
]);
let input = &mut IOChunk::new_from_str("foo42");
@@ -393,14 +440,26 @@ mod tests {
let cmd = Command::default();
let result = template.apply_captures(&cmd, &caps).unwrap();
assert_eq!(result, "got: foo42");
assert_eq!(result, b"got: foo42");
}
#[test]
fn test_apply_match_uses_matched_bytes() {
let template = ReplacementTemplate::new(vec![
ReplacementPart::Literal(b"<".to_vec()),
ReplacementPart::WholeMatch,
ReplacementPart::Literal(b">".to_vec()),
]);
let m = Match::from_bytes(1, 3, b"\xE9x");
assert_eq!(template.apply_match(&m), b"<\xE9x>");
}
#[test]
// s/foo(\d+)/number: \1/
fn test_backreference() {
let template = ReplacementTemplate::new(vec![
ReplacementPart::Literal("number: ".into()),
ReplacementPart::Literal(b"number: ".to_vec()),
ReplacementPart::Group(1),
]);
let input = &mut IOChunk::new_from_str("foo42");
@@ -408,16 +467,16 @@ mod tests {
let cmd = Command::default();
let result = template.apply_captures(&cmd, &caps).unwrap();
assert_eq!(result, "number: 42");
assert_eq!(result, b"number: 42");
}
#[test]
// s/(\w+):(\d+)/key: \1, value: \2/
fn test_multiple_parts() {
let template = ReplacementTemplate::new(vec![
ReplacementPart::Literal("key: ".into()),
ReplacementPart::Literal(b"key: ".to_vec()),
ReplacementPart::Group(1),
ReplacementPart::Literal(", value: ".into()),
ReplacementPart::Literal(b", value: ".to_vec()),
ReplacementPart::Group(2),
]);
let input = &mut IOChunk::new_from_str("x:123");
@@ -425,16 +484,16 @@ mod tests {
let cmd = Command::default();
let result = template.apply_captures(&cmd, &caps).unwrap();
assert_eq!(result, "key: x, value: 123");
assert_eq!(result, b"key: x, value: 123");
}
#[test]
// s/(\w+):(\d+)/key: \1, value: \3/
fn test_invalid_group() {
let template = ReplacementTemplate::new(vec![
ReplacementPart::Literal("key: ".into()),
ReplacementPart::Literal(b"key: ".to_vec()),
ReplacementPart::Group(1),
ReplacementPart::Literal(", value: ".into()),
ReplacementPart::Literal(b", value: ".to_vec()),
ReplacementPart::Group(3),
]);
let input = &mut IOChunk::new_from_str("x:123");
@@ -452,11 +511,11 @@ mod tests {
#[test]
fn test_max_group_number_with_groups() {
let template = ReplacementTemplate::new(vec![
ReplacementPart::Literal("a".into()),
ReplacementPart::Literal(b"a".to_vec()),
ReplacementPart::Group(2),
ReplacementPart::WholeMatch,
ReplacementPart::Group(5),
ReplacementPart::Literal("z".into()),
ReplacementPart::Literal(b"z".to_vec()),
]);
assert_eq!(template.max_group_number, 5);
}
@@ -464,9 +523,9 @@ mod tests {
#[test]
fn test_max_group_number_without_groups() {
let template = ReplacementTemplate::new(vec![
ReplacementPart::Literal("no".into()),
ReplacementPart::Literal(b"no".to_vec()),
ReplacementPart::WholeMatch,
ReplacementPart::Literal("groups".into()),
ReplacementPart::Literal(b"groups".to_vec()),
]);
assert_eq!(template.max_group_number, 0);
}
@@ -476,16 +535,31 @@ mod tests {
#[test]
fn test_identity_lookup_fast_path() {
let t = Transliteration::default();
assert_eq!(t.lookup('A'), 'A');
assert_eq!(t.lookup('z'), 'z');
assert_eq!(t.lookup('\u{07FF}'), '\u{07FF}'); // highest 2-byte UTF-8 char
assert_eq!(t.lookup_char('A'), 'A');
assert_eq!(t.lookup_char('z'), 'z');
assert_eq!(t.lookup_char('\u{07FF}'), '\u{07FF}'); // highest 2-byte UTF-8 char
}
#[test]
fn test_identity_lookup_slow_path() {
let t = Transliteration::default();
assert_eq!(t.lookup('\u{0800}'), '\u{0800}'); // just outside fast path
assert_eq!(t.lookup('\u{1F600}'), '\u{1F600}'); // 😀
assert_eq!(t.lookup_char('\u{0800}'), '\u{0800}'); // just outside fast path
assert_eq!(t.lookup_char('\u{1F600}'), '\u{1F600}'); // 😀
}
#[test]
fn test_from_bytes_and_lookup_byte() {
let t = Transliteration::from_bytes(b"a\xE9", b"Z!");
assert_eq!(t.lookup_byte(b'a'), b'Z');
assert_eq!(t.lookup_byte(0xE9), b'!');
assert_eq!(t.lookup_byte(b'b'), b'b');
}
#[test]
fn test_is_byte_identity_tracks_non_ascii_character_mappings() {
assert!(Transliteration::from_strings("ab", "xy").is_byte_identity);
assert!(!Transliteration::from_strings("aé", "xy").is_byte_identity);
assert!(!Transliteration::from_strings("ab", "xé").is_byte_identity);
}
#[test]
@@ -493,38 +567,38 @@ mod tests {
let mut t = Transliteration::default();
t.insert('a', 'α');
t.insert('b', 'β');
assert_eq!(t.lookup('a'), 'α');
assert_eq!(t.lookup('b'), 'β');
assert_eq!(t.lookup('c'), 'c'); // unchanged
assert_eq!(t.lookup_char('a'), 'α');
assert_eq!(t.lookup_char('b'), 'β');
assert_eq!(t.lookup_char('c'), 'c'); // unchanged
}
#[test]
fn test_insert_and_lookup_slow_path() {
let mut t = Transliteration::default();
t.insert('🦀', 'c'); // U+1F980 Crab emoji -> 'c'
assert_eq!(t.lookup('🦀'), 'c');
assert_eq!(t.lookup('🦁'), '🦁'); // unchanged
assert_eq!(t.lookup_char('🦀'), 'c');
assert_eq!(t.lookup_char('🦁'), '🦁'); // unchanged
}
#[test]
fn test_overwrite_mapping() {
let mut t = Transliteration::default();
t.insert('x', '1');
assert_eq!(t.lookup('x'), '1');
assert_eq!(t.lookup_char('x'), '1');
t.insert('x', '2');
assert_eq!(t.lookup('x'), '2');
assert_eq!(t.lookup_char('x'), '2');
}
#[test]
fn test_all_fast_path_mapped_to_space() {
let mut t = Transliteration::default();
for cp in 0..COMMON_UNICODE {
if let Some(ch) = char::from_u32(cp as u32) {
if let Some(ch) = u32::try_from(cp).ok().and_then(char::from_u32) {
t.insert(ch, ' ');
}
}
assert_eq!(t.lookup('A'), ' ');
assert_eq!(t.lookup('\u{07FF}'), ' ');
assert_eq!(t.lookup_char('A'), ' ');
assert_eq!(t.lookup_char('\u{07FF}'), ' ');
}
// from_strings
@@ -532,11 +606,11 @@ mod tests {
fn test_basic_transliteration() {
let t = Transliteration::from_strings("abcδ", "1234");
assert_eq!(t.lookup('a'), '1');
assert_eq!(t.lookup('b'), '2');
assert_eq!(t.lookup('c'), '3');
assert_eq!(t.lookup('δ'), '4');
assert_eq!(t.lookup('e'), 'e'); // not mapped, fallback
assert_eq!(t.lookup_char('a'), '1');
assert_eq!(t.lookup_char('b'), '2');
assert_eq!(t.lookup_char('c'), '3');
assert_eq!(t.lookup_char('δ'), '4');
assert_eq!(t.lookup_char('e'), 'e'); // not mapped, fallback
}
#[test]
@@ -545,16 +619,16 @@ mod tests {
let target = "e文c";
let t = Transliteration::from_strings(source, target);
assert_eq!(t.lookup('é'), 'e');
assert_eq!(t.lookup('漢'), '文');
assert_eq!(t.lookup('🦀'), 'c');
assert_eq!(t.lookup('x'), 'x'); // fast fallback
assert_eq!(t.lookup('文'), '文'); // slow fallback
assert_eq!(t.lookup_char('é'), 'e');
assert_eq!(t.lookup_char('漢'), '文');
assert_eq!(t.lookup_char('🦀'), 'c');
assert_eq!(t.lookup_char('x'), 'x'); // fast fallback
assert_eq!(t.lookup_char('文'), '文'); // slow fallback
}
#[test]
fn test_overwrite_fast_path() {
let t = Transliteration::from_strings("aa", "12");
assert_eq!(t.lookup('a'), '2'); // last mapping wins
assert_eq!(t.lookup_char('a'), '2'); // last mapping wins
}
}
+551 -150
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+233 -103
View File
File diff suppressed because it is too large. Load diff
+217 -87
View File
File diff suppressed because it is too large. Load diff
+103 -18
View File
@@ -20,18 +20,20 @@ pub mod processor;
pub mod script_char_provider;
pub mod script_line_provider;
use crate::sed::command::{ProcessingContext, StringSpace};
use crate::sed::command::{ByteSpace, CharacterMode, ProcessingContext};
use crate::sed::compiler::compile;
use crate::sed::processor::process_all_files;
use crate::sed::script_line_provider::ScriptValue;
use clap::{Arg, ArgMatches, Command, arg, crate_version};
use clap::{Arg, ArgMatches, Command, arg};
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use uucore::error::{UResult, UUsageError};
use uucore::error::{UResult, USimpleError, UUsageError};
use uucore::format_usage;
const ABOUT: &str = "Stream editor for filtering and transforming text";
const ABOUT: &str = "Stream editor for filtering and transforming text (part of uutils)";
const USAGE: &str = "sed [OPTION]... [script] [file]...";
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (uutils)");
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@@ -46,7 +48,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
let (scripts, files) = get_scripts_files(&matches)?;
let mut context = build_context(&matches);
let mut context = build_context(&matches)?;
let executable = compile(scripts, &mut context)?;
process_all_files(executable, files, &mut context)?;
@@ -61,7 +63,7 @@ pub fn uu_app() -> Command {
let util_name = uucore::util_name();
Command::new(util_name)
.version(crate_version!())
.version(VERSION)
.about(ABOUT)
.override_usage(format_usage(USAGE))
.args_override_self(true)
@@ -192,9 +194,40 @@ fn get_scripts_files(matches: &ArgMatches) -> UResult<(Vec<ScriptValue>, Vec<Pat
Ok((scripts, files))
}
// Parse CLI flag arguments and return a ProcessingContext struct based on them
fn build_context(matches: &ArgMatches) -> ProcessingContext {
ProcessingContext {
/// Return the character interpretation mode implied by the process locale.
///
/// The mode is determined from the first non-empty of LC_ALL, LC_CTYPE, and
/// LANG. The C and POSIX locales select byte mode. UTF-8 locales (including
/// C.UTF-8) select UTF-8 mode. All other locales result in an error.
pub fn character_mode_for_locale(locale: &str) -> UResult<CharacterMode> {
if locale == "C" || locale == "POSIX" {
Ok(CharacterMode::Byte)
} else if locale.eq_ignore_ascii_case("C.UTF-8")
|| locale.to_ascii_lowercase().ends_with(".utf-8")
|| locale.to_ascii_lowercase().ends_with(".utf8")
{
Ok(CharacterMode::Utf8)
} else {
Err(USimpleError::new(
1,
format!("unsupported locale: {locale}"),
))
}
}
/// Return a ProcessingContext based on parsed CLI flags and environment.
fn build_context(matches: &ArgMatches) -> UResult<ProcessingContext> {
let locale = ["LC_ALL", "LC_CTYPE", "LANG"]
.into_iter()
.find_map(|name| {
let value = env::var(name).ok()?;
(!value.is_empty()).then_some(value)
})
// Same default as GNU sed
.unwrap_or_else(|| "C".to_string());
Ok(ProcessingContext {
// CLI arguments
all_output_files: matches.get_flag("all-output-files"),
debug: matches.get_flag("debug"),
regex_extended: matches.get_flag("regexp-extended"),
@@ -211,6 +244,9 @@ fn build_context(matches: &ArgMatches) -> ProcessingContext {
unbuffered: matches.get_flag("unbuffered"),
null_data: matches.get_flag("null-data"),
// Environment
character_mode: character_mode_for_locale(&locale)?,
// Other context
input_name: "<stdin>".to_string(),
line_number: 0,
@@ -220,8 +256,8 @@ fn build_context(matches: &ArgMatches) -> ProcessingContext {
stop_processing: false,
saved_regex: None,
input_action: None,
hold: StringSpace {
content: String::new(),
hold: ByteSpace {
content: Vec::new(),
has_newline: true,
},
parsed_block_nesting: 0,
@@ -229,7 +265,7 @@ fn build_context(matches: &ArgMatches) -> ProcessingContext {
range_commands: Vec::new(),
substitution_made: false,
append_elements: Vec::new(),
}
})
}
#[cfg(test)]
@@ -338,7 +374,7 @@ mod tests {
#[test]
fn test_defaults() {
let matches = test_matches(&[]);
let ctx = build_context(&matches);
let ctx = build_context(&matches).unwrap();
assert!(!ctx.all_output_files);
assert!(!ctx.debug);
@@ -373,7 +409,7 @@ mod tests {
"-z",
]);
let ctx = build_context(&matches);
let ctx = build_context(&matches).unwrap();
assert!(ctx.all_output_files);
assert!(ctx.debug);
@@ -393,7 +429,7 @@ mod tests {
#[test]
fn test_multiple_same_arguments() {
let matches = test_matches(&["-E", "-r"]);
let ctx = build_context(&matches);
let ctx = build_context(&matches).unwrap();
assert!(ctx.regex_extended);
}
@@ -401,7 +437,7 @@ mod tests {
#[test]
fn test_in_place_with_suffix() {
let matches = test_matches(&["-i", ".bak"]);
let ctx = build_context(&matches);
let ctx = build_context(&matches).unwrap();
assert!(ctx.in_place);
assert_eq!(ctx.in_place_suffix, Some(".bak".to_string()));
@@ -412,10 +448,59 @@ mod tests {
let matches_default = test_matches(&[]);
let matches_custom = test_matches(&["-l", "120"]);
let ctx_default = build_context(&matches_default);
let ctx_custom = build_context(&matches_custom);
let ctx_default = build_context(&matches_default).unwrap();
let ctx_custom = build_context(&matches_custom).unwrap();
assert_eq!(ctx_default.length, 70);
assert_eq!(ctx_custom.length, 120);
}
#[test]
fn c_locale_selects_byte_mode() {
assert_eq!(character_mode_for_locale("C").unwrap(), CharacterMode::Byte);
}
#[test]
fn posix_locale_selects_byte_mode() {
assert_eq!(
character_mode_for_locale("POSIX").unwrap(),
CharacterMode::Byte
);
}
#[test]
fn c_utf8_locale_selects_utf8_mode() {
assert_eq!(
character_mode_for_locale("C.UTF-8").unwrap(),
CharacterMode::Utf8
);
}
#[test]
fn dot_utf8_locale_selects_utf8_mode() {
assert_eq!(
character_mode_for_locale("en_US.UTF-8").unwrap(),
CharacterMode::Utf8
);
}
#[test]
fn dot_utf8_locale_is_case_insensitive() {
assert_eq!(
character_mode_for_locale("el_GR.utf8").unwrap(),
CharacterMode::Utf8
);
}
#[test]
fn unsupported_locale_reports_locale_name() {
let err = character_mode_for_locale("el_GR.ISO-8859-7").unwrap_err();
assert_eq!(err.code(), 1);
assert!(
err.to_string()
.contains("unsupported locale: el_GR.ISO-8859-7"),
"{err}"
);
}
}
+34 -7
View File
@@ -57,13 +57,21 @@ impl NamedWriter {
/// Write a line to the file with a newline, returning descriptive errors.
pub fn write_line(&mut self, line: &str) -> UResult<()> {
writeln!(self.writer, "{line}").map_err(|e| {
runtime_error::<()>(
&self.location,
format!("writing to file {}: {e}", self.path.quote()),
)
.unwrap_err()
})
self.write_line_bytes(line.as_bytes())
}
/// Write bytes to the file with a newline, returning descriptive errors.
pub fn write_line_bytes(&mut self, line: &[u8]) -> UResult<()> {
self.writer
.write_all(line)
.and_then(|()| self.writer.write_all(b"\n"))
.map_err(|e| {
runtime_error::<()>(
&self.location,
format!("writing to file {}: {e}", self.path.quote()),
)
.unwrap_err()
})
}
/// Flush the writer, returning a descriptive error.
@@ -88,3 +96,22 @@ pub fn flush_all() -> UResult<()> {
Ok(())
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::NamedTempFile;
#[test]
fn test_write_line_bytes_appends_newline() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
let writer = NamedWriter::new(path.clone(), ScriptLocation::default()).unwrap();
writer.borrow_mut().write_line_bytes(b"a\xE9").unwrap();
writer.borrow_mut().flush().unwrap();
assert_eq!(fs::read(path).unwrap(), b"a\xE9\n");
}
}
+379 -115
View File
File diff suppressed because it is too large. Load diff
+38 -8
View File
@@ -1,4 +1,4 @@
// Provide the script contents character by character
// Provide the script contents byte by byte
//
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 Diomidis Spinellis
@@ -10,19 +10,20 @@
#[derive(Debug)]
pub struct ScriptCharProvider {
line: Vec<char>,
line: Vec<u8>,
pos: usize,
}
impl ScriptCharProvider {
pub fn new(line_string: &str) -> Self {
/// Build from raw script bytes.
pub fn new(line: impl AsRef<[u8]>) -> Self {
Self {
line: line_string.chars().collect(),
line: line.as_ref().to_vec(),
pos: 0,
}
}
/// Advances to the next character, if not at end of line.
/// Advances to the next byte, if not at end of line.
pub fn advance(&mut self) {
if self.pos < self.line.len() {
self.pos += 1;
@@ -39,8 +40,13 @@ impl ScriptCharProvider {
self.pos = pos;
}
/// Returns the current character. Panics if out of bounds.
/// Returns the current byte as a character. Panics if out of bounds.
pub fn current(&self) -> char {
char::from(self.line[self.pos])
}
/// Returns the current script byte. Panics if out of bounds.
pub fn current_byte(&self) -> u8 {
self.line[self.pos]
}
@@ -51,8 +57,8 @@ impl ScriptCharProvider {
/// Advances the position past any whitespace characters.
pub fn eat_spaces(&mut self) {
while self.pos < self.line.len() && self.line[self.pos].is_whitespace() {
self.pos += 1;
while self.pos < self.line.len() && self.current().is_whitespace() {
self.advance();
}
}
@@ -139,4 +145,28 @@ mod tests {
assert_eq!(chars.get_pos(), 2);
assert_eq!(chars.current(), 'c');
}
#[test]
fn test_current_for_utf8_bytes() {
let mut chars = ScriptCharProvider::new("αb");
assert_eq!(chars.current(), char::from(0xCE));
assert_eq!(chars.current_byte(), 0xCE);
chars.advance();
assert_eq!(chars.current(), char::from(0xB1));
assert_eq!(chars.current_byte(), 0xB1);
chars.advance();
assert_eq!(chars.current(), 'b');
}
#[test]
fn test_current_for_invalid_utf8_byte() {
let mut chars = ScriptCharProvider::new(vec![0xC2, b'a']);
assert_eq!(chars.current(), char::from(0xC2));
assert_eq!(chars.current_byte(), 0xC2);
chars.advance();
assert_eq!(chars.current(), 'a');
assert_eq!(chars.current_byte(), b'a');
}
}
+22 -11
View File
@@ -69,8 +69,8 @@ impl ScriptLineProvider {
}
/// Return the next script line to process across all scripts.
pub fn next_line(&mut self) -> UResult<Option<String>> {
let mut line = String::new();
pub fn next_line(&mut self) -> UResult<Option<Vec<u8>>> {
let mut line = Vec::new();
loop {
let advance = match &mut self.state {
@@ -82,13 +82,13 @@ impl ScriptLineProvider {
..
} => {
line.clear();
let bytes = reader.read_line(&mut line)?;
let bytes = reader.read_until(b'\n', &mut line)?;
if bytes == 0 {
Some(*index + 1) // finished reading this source
} else {
*line_number += 1;
// Remove trailing newline
if line.ends_with('\n') {
if line.ends_with(b"\n") {
line.pop();
}
return Ok(Some(line));
@@ -114,7 +114,7 @@ impl ScriptLineProvider {
match &self.sources[next_index] {
ScriptValue::StringVal(s) => {
let cursor = std::io::Cursor::new(s.clone());
let cursor = std::io::Cursor::new(s.as_bytes().to_vec());
self.state = State::Active {
index: next_index,
reader: Box::new(BufReader::new(cursor)),
@@ -199,7 +199,7 @@ mod tests {
let mut lines = Vec::new();
while let Some(line) = provider.next_line().unwrap() {
lines.push(line.trim_end().to_string());
lines.push(String::from_utf8(line).unwrap().trim_end().to_string());
}
assert_eq!(lines, vec!["line one", "line two", "line three"]);
@@ -216,7 +216,7 @@ mod tests {
let mut lines = Vec::new();
while let Some(line) = provider.next_line().unwrap() {
lines.push(line.trim_end().to_string());
lines.push(String::from_utf8(line).unwrap().trim_end().to_string());
}
assert_eq!(lines, vec!["file line 1", "file line 2"]);
@@ -241,7 +241,7 @@ mod tests {
let mut lines = Vec::new();
while let Some(line) = provider.next_line().unwrap() {
lines.push(line.trim_end().to_string());
lines.push(String::from_utf8(line).unwrap().trim_end().to_string());
}
assert_eq!(
@@ -266,7 +266,7 @@ mod tests {
let mut provider = ScriptLineProvider::new(input);
if let Some(line) = provider.next_line().unwrap() {
assert_eq!(line.trim(), "l1");
assert_eq!(String::from_utf8(line).unwrap().trim(), "l1");
assert_eq!(provider.get_line_number(), 1);
assert_eq!(provider.get_input_name(), "<script argument 1>");
} else {
@@ -274,7 +274,7 @@ mod tests {
}
if let Some(line) = provider.next_line().unwrap() {
assert_eq!(line.trim(), "l2");
assert_eq!(String::from_utf8(line).unwrap().trim(), "l2");
assert_eq!(provider.get_line_number(), 2);
assert_eq!(provider.get_input_name(), "<script argument 1>");
} else {
@@ -282,11 +282,22 @@ mod tests {
}
if let Some(line) = provider.next_line().unwrap() {
assert_eq!(line.trim(), "l3");
assert_eq!(String::from_utf8(line).unwrap().trim(), "l3");
assert_eq!(provider.get_line_number(), 1);
assert_eq!(provider.get_input_name(), "<script argument 2>");
} else {
panic!("Expected a line");
}
}
#[test]
fn test_file_source_preserves_invalid_utf8_bytes() {
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"s/\xC2\xE7/X/\n").unwrap();
let input = vec![ScriptValue::PathVal(temp_file.path().to_path_buf())];
let mut provider = ScriptLineProvider::new(input);
assert_eq!(provider.next_line().unwrap().unwrap(), b"s/\xC2\xE7/X/");
}
}
+606 -11
View File
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
CLEARED ^l1_1
CLEARED ^l1_2
CLEARED ^l1_3
CLEARED ^l1_4
CLEARED ^l1_5
CLEARED ^l1_6
CLEARED ^l1_7
CLEARED ^l1_8
CLEARED ^l1_9
CLEARED ^l1_10
CLEARED ^l1_11
CLEARED ^l1_12
CLEARED ^l1_13
CLEARED ^l1_14
+14
View File
@@ -0,0 +1,14 @@
l1_1
SUB l1_X
l1_3
l1_4
l1_5
l1_6
l1_7
l1_8
l1_9
l1_10
l1_11
SUB l1_1X
l1_13
l1_14
Loaded 20 of 21 files, more files were not shown because too many files have changed in this diff. Show more