diff --git a/.gitignore b/.gitignore index 131b2b894e..ee57734872 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,6 @@ nonos-bootloader/keys # Per-user kernel build configuration written by tools/nonos-config .nonos-config + +# Build output from vendored crates.io tool sources +userland/upstream-src/*/target/ diff --git a/Cargo.toml b/Cargo.toml index c38fe9b3a7..7856e935ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,12 @@ nonos-capsule-std-proof = [] nonos-capsule-ripgrep = [] nonos-capsule-sd = [] nonos-capsule-tokio-smoke = [] +# Bake the installed crates.io tools (grex, tokei, ...) into the image. Off for +# core builds, which do not cross-compile the tool binaries; on for the desktop. +nonos-tool-capsules = [] +# Boot self-test: run each installed tool once with --version/-h so its output +# lands on the serial log, proving the whole run path end to end. Test builds only. +nonos-tool-selftest = ["nonos-tool-capsules"] nonos-capsule-ramfs = [] nonos-capsule-wallpaper = [] nonos-capsule-compositor = [] @@ -494,6 +500,7 @@ microkernel-desktop-gui = [ "microkernel-core", "nonos-production", "capsule-serial-debug", + "nonos-tool-capsules", "nonos-capsule-proof-io", "nonos-capsule-tokio-smoke", "nonos-capsule-ramfs", diff --git a/mk/10-qemu.mk b/mk/10-qemu.mk index b58b5d79e7..e14f5e7801 100644 --- a/mk/10-qemu.mk +++ b/mk/10-qemu.mk @@ -62,7 +62,7 @@ ifeq ($(QEMU_GL),1) QEMU_GPU := -device virtio-vga-gl,xres=$(QEMU_XRES),yres=$(QEMU_YRES) QEMU_DISPLAY := cocoa,gl=es,zoom-to-fit=on else -QEMU_GPU := -device virtio-vga,disable-modern=on,vectors=0,xres=$(QEMU_XRES),yres=$(QEMU_YRES) +QEMU_GPU := -device virtio-vga,disable-modern=on,vectors=0,edid=on,xres=$(QEMU_XRES),yres=$(QEMU_YRES) QEMU_DISPLAY := cocoa,zoom-to-fit=on endif # Keyboard/mouse via the q35 i8042 (PS/2). USB HID interrupt-IN transfers diff --git a/mk/20-build.mk b/mk/20-build.mk index 97c50188b8..460d68b6c7 100644 --- a/mk/20-build.mk +++ b/mk/20-build.mk @@ -293,6 +293,38 @@ $(UPSTREAM_TOKIO_SMOKE_BIN): $(NONOS_RT_OBJ) $(NONOS_STD_PAL_STAMP) \ .PHONY: nonos-mk-upstream-tokio-smoke nonos-mk-upstream-tokio-smoke: $(UPSTREAM_TOKIO_SMOKE_BIN) +# Installed crates.io command-line tools, cross-compiled unmodified for +# x86_64-nonos from the byte-identical source vendored under upstream-src (cargo +# only honors [patch] for path builds, so shimmed crates must build with --path). +# Each tool's Capsule.mk consumes target/upstream-/bin/; this template +# is the one rule that builds it, so adding a tool is a name in NONOS_TOOL_BINS +# plus its `nonos-app add`, not a new recipe. getrandom-backed tools take the +# RDRAND backend, which nonos x86_64 always has. +NONOS_TOOL_BINS := grex dotenv-linter pastel jsonxf tokei huniq csview + +# Default is to strip default features (they pull unix-only or update-check +# extras nonos does not want). grex and tokei gate their binary behind a `cli` +# feature, so those two re-enable it. Per-tool overrides live in _CARGO_FEATURES. +NONOS_TOOL_FEATURES_DEFAULT := --no-default-features +grex_CARGO_FEATURES := --no-default-features --features cli +tokei_CARGO_FEATURES := --no-default-features --features cli + +define nonos_upstream_tool_rule +$(TARGET_DIR)/upstream-$(1)/bin/$(1): $(NONOS_RT_OBJ) $(NONOS_STD_PAL_STAMP) \ + userland/x86_64-nonos-user.json | $(TARGET_DIR)/.nonos-toolchain.stamp + @echo "Building upstream $(1) for NONOS (unmodified crates.io source)..." + @cd userland/upstream-src/$(1) && RUSTUP_TOOLCHAIN=$(TOOLCHAIN) \ + RUSTFLAGS="-Clink-arg=$(abspath $(NONOS_RT_OBJ)) --cfg getrandom_backend=\"rdrand\"" \ + $(CARGO) install --path . $(or $($(1)_CARGO_FEATURES),$(NONOS_TOOL_FEATURES_DEFAULT)) \ + --target $(abspath userland/x86_64-nonos-user.json) \ + -Zbuild-std=std,panic_abort -Zbuild-std-features=compiler-builtins-mem \ + --root $(abspath $(TARGET_DIR)/upstream-$(1)) --no-track --force --bin $(1) +endef +$(foreach t,$(NONOS_TOOL_BINS),$(eval $(call nonos_upstream_tool_rule,$(t)))) + +.PHONY: nonos-mk-upstream-tools +nonos-mk-upstream-tools: $(foreach t,$(NONOS_TOOL_BINS),$(TARGET_DIR)/upstream-$(t)/bin/$(t)) + $(CAPSULE_SIGN_BIN): @echo "Building capsule-sign host tool..." @cd nonos-sign && cargo build --release --bin capsule-sign @@ -354,6 +386,13 @@ include userland/capsule_proof_io/Capsule.mk include userland/capsule_std_proof/Capsule.mk include userland/capsule_ripgrep/Capsule.mk include userland/capsule_sd/Capsule.mk +include userland/capsule_csview/Capsule.mk +include userland/capsule_huniq/Capsule.mk +include userland/capsule_tokei/Capsule.mk +include userland/capsule_jsonxf/Capsule.mk +include userland/capsule_pastel/Capsule.mk +include userland/capsule_dotenv-linter/Capsule.mk +include userland/capsule_grex/Capsule.mk include userland/capsule_tokio_smoke/Capsule.mk include userland/capsule_ramfs/Capsule.mk include userland/capsule_keyring/Capsule.mk @@ -774,6 +813,13 @@ nonos-mk-audio-player-smoketest-dev-test: $(proof-io_MANIFEST) $(audio_MANIFEST) # is what ships and what the ISO carries; do not fold this into it. nonos-mk-desktop-gui-prod: $(proof-io_ARTIFACTS) \ $(std-proof_ARTIFACTS) $(ripgrep_ARTIFACTS) $(sd_ARTIFACTS) \ + $(csview_ARTIFACTS) \ + $(huniq_ARTIFACTS) \ + $(tokei_ARTIFACTS) \ + $(jsonxf_ARTIFACTS) \ + $(pastel_ARTIFACTS) \ + $(dotenv-linter_ARTIFACTS) \ + $(grex_ARTIFACTS) \ $(tokio-smoke_ARTIFACTS) $(ramfs_ARTIFACTS) \ $(keyring_ARTIFACTS) $(entropy_ARTIFACTS) $(crypto_ARTIFACTS) \ $(vfs_ARTIFACTS) $(driver-virtio-rng_ARTIFACTS) \ diff --git a/screenshots/desktop.png b/screenshots/desktop.png new file mode 100644 index 0000000000..e7b65c3074 Binary files /dev/null and b/screenshots/desktop.png differ diff --git a/screenshots/launchpad.png b/screenshots/launchpad.png new file mode 100644 index 0000000000..4856b55f1e Binary files /dev/null and b/screenshots/launchpad.png differ diff --git a/src/services/registry.rs b/src/services/registry.rs index d96788fd3b..b1726c0fdf 100644 --- a/src/services/registry.rs +++ b/src/services/registry.rs @@ -32,8 +32,14 @@ pub(crate) use reserved::is_reserved_service; pub const MAX_SERVICES: usize = 256; static ENDPOINTS: Mutex> = Mutex::new(Vec::new()); +/// Register a service endpoint on behalf of `pid`. This is the trusted core +/// path: the kernel spawn path calls it to publish a verified capsule's own +/// declared endpoint, so the only authorization it enforces is that `pid` +/// actually holds the capabilities the endpoint advertises. The runtime +/// `sys_service_register` syscall layers the caller-side register-right check +/// on top before reaching here (see `caller_has_register_right`). pub fn register_endpoint(name: &str, port: u32, pid: u32, caps: u64) -> Result<(), RegError> { - if !auth::caller_can_register(pid, caps) { + if !auth::owner_has_required(pid, caps) { return Err(RegError::PermissionDenied); } let mut eps = ENDPOINTS.lock(); @@ -50,6 +56,14 @@ pub fn register_endpoint(name: &str, port: u32, pid: u32, caps: u64) -> Result<( Ok(()) } +/// True if the current process is allowed to register a service name it does +/// not already own. Gates the runtime `sys_service_register` syscall so an +/// ordinary capsule cannot squat a peer's service; publishers granted +/// `RegisterService` (e.g. net.core) pass. +pub(crate) fn caller_has_register_right() -> bool { + auth::caller_has_register_right() +} + pub fn lookup_service(name: &str) -> Option { ENDPOINTS.lock().iter().find(|e| e.name == name).cloned() } diff --git a/src/services/registry/auth/caller_can_register.rs b/src/services/registry/auth/caller_can_register.rs deleted file mode 100644 index a14c8d3b41..0000000000 --- a/src/services/registry/auth/caller_can_register.rs +++ /dev/null @@ -1,33 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -use super::caller_has_register_right::caller_has_register_right; -use super::owner_has_required::owner_has_required; - -pub(in crate::services::registry) fn caller_can_register(owner_pid: u32, required: u64) -> bool { - if !owner_has_required(owner_pid, required) { - return false; - } - // Kernel-context registration (boot-time trusted services) has no current - // pid and is allowed. Every capsule with a pid must hold the register right; - // the old `pid <= 64` shortcut handed unauthenticated registration authority - // to any capsule spawned early enough to land in that range, which let an - // ordinary app squat/impersonate a service name. - match crate::process::current_pid() { - None => true, - Some(_) => caller_has_register_right(), - } -} diff --git a/src/services/registry/auth/caller_has_register_right.rs b/src/services/registry/auth/caller_has_register_right.rs index ad09183be5..202d7f066b 100644 --- a/src/services/registry/auth/caller_has_register_right.rs +++ b/src/services/registry/auth/caller_has_register_right.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pub(super) fn caller_has_register_right() -> bool { +pub(in crate::services::registry) fn caller_has_register_right() -> bool { let token = crate::syscall::capabilities::current_caps_or_default(); token.can_register_service() || token.is_admin() } diff --git a/src/services/registry/auth/mod.rs b/src/services/registry/auth/mod.rs index b59b326875..468b0f6754 100644 --- a/src/services/registry/auth/mod.rs +++ b/src/services/registry/auth/mod.rs @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -mod caller_can_register; mod caller_has_register_right; mod owner_has_required; -pub(in crate::services::registry) use caller_can_register::caller_can_register; +pub(in crate::services::registry) use caller_has_register_right::caller_has_register_right; +pub(in crate::services::registry) use owner_has_required::owner_has_required; diff --git a/src/services/registry/auth/owner_has_required.rs b/src/services/registry/auth/owner_has_required.rs index bbfac2defb..24a29b9681 100644 --- a/src/services/registry/auth/owner_has_required.rs +++ b/src/services/registry/auth/owner_has_required.rs @@ -14,6 +14,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -pub(super) fn owner_has_required(owner_pid: u32, required: u64) -> bool { +pub(in crate::services::registry) fn owner_has_required(owner_pid: u32, required: u64) -> bool { required == 0 || crate::process::caps::has(owner_pid, required) } diff --git a/src/syscall/contract/cap_table/mk.rs b/src/syscall/contract/cap_table/mk.rs index 6a5451e654..2d2ebea4e1 100644 --- a/src/syscall/contract/cap_table/mk.rs +++ b/src/syscall/contract/cap_table/mk.rs @@ -103,6 +103,11 @@ pub(super) fn check(caps: &CapabilityToken, number: SyscallNumber) -> Option caps.can_spawn_window(), + // Running a baked command-line tool needs only IPC: the tool is spawned + // parented to the caller so the caller can drive its stdio, and only the + // baked, attested set can be named. + SyscallNumber::MkToolRun => caps.can_ipc(), + _ => return None, }) } diff --git a/src/syscall/dispatch/router/microkernel_ops.rs b/src/syscall/dispatch/router/microkernel_ops.rs index d9abf226d9..81e68ce3c7 100644 --- a/src/syscall/dispatch/router/microkernel_ops.rs +++ b/src/syscall/dispatch/router/microkernel_ops.rs @@ -74,6 +74,7 @@ pub(super) fn matches(nr: SyscallNumber) -> bool { | MkPioRelease | MkDebug | MkSpawnInstance + | MkToolRun ) } diff --git a/src/syscall/microkernel/dispatch/process.rs b/src/syscall/microkernel/dispatch/process.rs index a14c2aebae..cc7d012335 100644 --- a/src/syscall/microkernel/dispatch/process.rs +++ b/src/syscall/microkernel/dispatch/process.rs @@ -30,6 +30,7 @@ use crate::syscall::microkernel::process::{ use crate::syscall::microkernel::futex::{sys_futex_wait, sys_futex_wake}; use crate::syscall::microkernel::procstat::sys_proc_stat; use crate::syscall::microkernel::spawn_instance::sys_spawn_instance; +use crate::syscall::microkernel::tool_run::sys_tool_run; use crate::syscall::microkernel::time::{ sys_time_adjust, sys_time_millis, sys_time_monotonic, sys_time_rtc, }; @@ -63,6 +64,7 @@ pub(super) fn handle(nr: u64, a: Args) -> Option { SYS_STDIN_READ => sys_stdin_read(a.a0, a.a1 as usize), SYS_ATTEST_STATUS => sys_attest_status(a.a0), SYS_SPAWN_INSTANCE => sys_spawn_instance(a.a0, a.a1), + SYS_TOOL_RUN => sys_tool_run(a.a0, a.a1, a.a2, a.a3), _ => return None, }) } diff --git a/src/syscall/microkernel/ipc/call/sys_ipc_call.rs b/src/syscall/microkernel/ipc/call/sys_ipc_call.rs index 8ebd3a46a9..38f132a2f9 100644 --- a/src/syscall/microkernel/ipc/call/sys_ipc_call.rs +++ b/src/syscall/microkernel/ipc/call/sys_ipc_call.rs @@ -57,11 +57,11 @@ pub fn sys_ipc_call( if crate::usercopy::validate_user_write(resp, resp_len).is_err() { return ERRNO_FAULT; } - // This call's correlation token. It is registered with the pending reply so - // the redirect-reply path (a service replying via `mk_ipc_send` to its fixed - // reply endpoint) stamps the reply with it, and it is sent on the request so - // an `mk_ipc_reply` service echoes it via `take_for_reply`. Either way the - // genuine reply carries this token; a forged injection can only carry 0. + // This call's correlation token, registered with the pending reply keyed by + // this caller's inbox. Both reply paths read it back from that same entry: + // the redirect path (a service replying via `mk_ipc_send` to its fixed reply + // endpoint) and the direct `mk_ipc_reply` path both stamp the reply with it. + // The genuine reply carries this token; a forged injection can only carry 0. let token = next_call_token(); let inbox = reply_inbox::for_pid(pid); let endpoint = lookup_port(ep as u32); @@ -75,7 +75,7 @@ pub fn sys_ipc_call( trace(pid, b"send", send_result); if send_result < 0 { if let Some(server_pid) = endpoint_pid { - pending_reply::remove(server_pid, &inbox); + let _ = pending_reply::remove(server_pid, &inbox); } return send_result; } @@ -83,7 +83,7 @@ pub fn sys_ipc_call( let recv_result = recv_reply_correlated(pid, &inbox, resp, resp_len, timeout, token); if recv_result < 0 { if let Some(server_pid) = endpoint_pid { - pending_reply::remove(server_pid, &inbox); + let _ = pending_reply::remove(server_pid, &inbox); } } if recv_result >= 24 && req_len >= 20 { diff --git a/src/syscall/microkernel/ipc/correlation.rs b/src/syscall/microkernel/ipc/correlation.rs deleted file mode 100644 index 33a0035e85..0000000000 --- a/src/syscall/microkernel/ipc/correlation.rs +++ /dev/null @@ -1,44 +0,0 @@ -// NONOS Operating System -// Copyright (C) 2026 NONOS Contributors -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -extern crate alloc; - -use alloc::collections::{BTreeMap, VecDeque}; -use spin::Mutex; - -const MAX_PENDING_PER_PID: usize = 16; - -static PENDING: Mutex>> = Mutex::new(BTreeMap::new()); - -pub(super) fn note_request(client_pid: u32, token: u64) { - if token == 0 { - return; - } - let mut map = PENDING.lock(); - let queue = map.entry(client_pid).or_insert_with(VecDeque::new); - if queue.len() >= MAX_PENDING_PER_PID { - queue.pop_front(); - } - queue.push_back(token); -} - -pub(super) fn take_for_reply(client_pid: u32) -> u64 { - let mut map = PENDING.lock(); - match map.get_mut(&client_pid) { - Some(queue) => queue.pop_front().unwrap_or(0), - None => 0, - } -} diff --git a/src/syscall/microkernel/ipc/mod.rs b/src/syscall/microkernel/ipc/mod.rs index eaf3d6349b..cfab526082 100644 --- a/src/syscall/microkernel/ipc/mod.rs +++ b/src/syscall/microkernel/ipc/mod.rs @@ -15,7 +15,6 @@ // along with this program. If not, see . mod call; -mod correlation; mod inbox_name; mod lookup; mod pending_reply; diff --git a/src/syscall/microkernel/ipc/pending_reply/remove.rs b/src/syscall/microkernel/ipc/pending_reply/remove.rs index 08ebb4518f..a4dcc49761 100644 --- a/src/syscall/microkernel/ipc/pending_reply/remove.rs +++ b/src/syscall/microkernel/ipc/pending_reply/remove.rs @@ -16,17 +16,24 @@ use super::state::PENDING; -pub(in crate::syscall::microkernel::ipc) fn remove(server_pid: u32, caller_inbox: &str) -> bool { +/// Remove the pending-reply entry for `caller_inbox` under `server_pid` and +/// return the per-call correlation token it carried. Matching is by the +/// caller's private inbox, so the token is the one that exact `mk_ipc_call` +/// registered, even when several callers have replies outstanding on the same +/// server. Returns `None` when there is no such pending call (a forged or +/// duplicate reply), which the caller treats as "drop". +pub(in crate::syscall::microkernel::ipc) fn remove( + server_pid: u32, + caller_inbox: &str, +) -> Option { let mut map = PENDING.lock(); - let Some(queue) = map.get_mut(&server_pid) else { - return false; - }; - let Some(pos) = queue.iter().position(|(_, inbox, _)| inbox == caller_inbox) else { - return false; - }; - queue.remove(pos); + let queue = map.get_mut(&server_pid)?; + let pos = queue.iter().position(|(_, inbox, _)| inbox == caller_inbox)?; + // main's entry is (caller_pid, inbox, token); return the token so the direct + // mk_ipc_reply path stamps the reply from the same entry it removes. + let (_, _, token) = queue.remove(pos)?; if queue.is_empty() { map.remove(&server_pid); } - true + Some(token) } diff --git a/src/syscall/microkernel/ipc/recv_from.rs b/src/syscall/microkernel/ipc/recv_from.rs index 68ced6b14b..4a7a579ad3 100644 --- a/src/syscall/microkernel/ipc/recv_from.rs +++ b/src/syscall/microkernel/ipc/recv_from.rs @@ -19,7 +19,6 @@ use crate::process::current_pid; use crate::syscall::microkernel::errnos::{ERRNO_FAULT, ERRNO_INVAL, ERRNO_NOENT, ERRNO_TIMEDOUT}; use core::sync::atomic::{AtomicU32, Ordering}; -use super::correlation::note_request; use super::inbox_name::resolve_for_recv; use super::sender_pid::from_envelope; @@ -94,7 +93,6 @@ fn deliver( sender_pid_out: u64, ) -> i64 { let sender_pid = from_envelope(&msg.from); - note_request(sender_pid, msg.correlation); trace_dequeue(pid, sender_pid); let copy_len = msg.data.len().min(len); if crate::usercopy::copy_to_user(buf, &msg.data[..copy_len]).is_err() { diff --git a/src/syscall/microkernel/ipc/register.rs b/src/syscall/microkernel/ipc/register.rs index cb5fa2a593..bbee468fe5 100644 --- a/src/syscall/microkernel/ipc/register.rs +++ b/src/syscall/microkernel/ipc/register.rs @@ -61,6 +61,17 @@ pub fn sys_service_register(name_ptr: u64, name_len: usize, port: u32) -> i64 { if crate::services::registry::is_reserved_service(name, port) { return ERRNO_PERM; } + // A capsule may confirm the endpoint the kernel already published for it at + // spawn (idempotent self-registration); claiming any other name requires the + // RegisterService right. This lets an ordinary capsule re-assert its own + // service without holding the right, keeps a publisher like net.core able to + // register its extra sub-endpoints, and still bars a runtime capsule from + // squatting a peer's name. + let owns_already = crate::services::registry::lookup_service(name) + .map_or(false, |e| e.pid == pid && e.port == port); + if !owns_already && !crate::services::registry::caller_has_register_right() { + return ERRNO_PERM; + } match register_endpoint(name, port, pid, required_caps(name, Capability::IPC.bit())) { Ok(()) => 0, Err(RegError::Full) => ERRNO_NOMEM, diff --git a/src/syscall/microkernel/ipc/reply.rs b/src/syscall/microkernel/ipc/reply.rs index 233a2cd8a9..078b6b67c5 100644 --- a/src/syscall/microkernel/ipc/reply.rs +++ b/src/syscall/microkernel/ipc/reply.rs @@ -23,7 +23,6 @@ use crate::syscall::microkernel::errnos::{ ERRNO_BUSY, ERRNO_FAULT, ERRNO_INVAL, ERRNO_NOENT, ERRNO_NOMEM, }; -use super::correlation::take_for_reply; use super::pending_reply; use super::reply_inbox; @@ -69,10 +68,14 @@ pub fn sys_ipc_reply(dest_pid: u64, buf: u64, len: usize) -> i64 { } let caller_pid = current_pid().unwrap_or(0); let dest = reply_inbox::for_pid(dest_pid as u32); - if !pending_reply::remove(caller_pid, &dest) { + // The token comes from the same pending-reply entry that authorizes this + // reply, matched by the caller's inbox. Using that entry's token (rather + // than a separate per-client FIFO) keeps the stamp correct when several + // callers have replies outstanding on this server at once. + let Some(token) = pending_reply::remove(caller_pid, &dest) else { trace(caller_pid, dest_pid, b"drop no-call", 0, &dest); return 0; - } + }; let from = alloc::format!("proc.{}", caller_pid); let mut msg = match IpcMessage::new(&from, &dest, &data) { Ok(msg) => msg, @@ -81,7 +84,7 @@ pub fn sys_ipc_reply(dest_pid: u64, buf: u64, len: usize) -> i64 { return ERRNO_NOMEM; } }; - msg.correlation = take_for_reply(dest_pid as u32); + msg.correlation = token; let rc = match try_enqueue_strict(&dest, msg) { Ok(()) => { crate::sched::wake_process(dest_pid as u32); diff --git a/src/syscall/microkernel/mod.rs b/src/syscall/microkernel/mod.rs index f7bc009d39..f8fe66b14b 100644 --- a/src/syscall/microkernel/mod.rs +++ b/src/syscall/microkernel/mod.rs @@ -42,6 +42,7 @@ pub mod process; pub mod procstat; pub mod spawn_instance; pub mod time; +pub mod tool_run; pub mod wait; pub use attest::sys_attest_status; diff --git a/src/syscall/microkernel/numbers.rs b/src/syscall/microkernel/numbers.rs index 630373b231..1b942249ba 100644 --- a/src/syscall/microkernel/numbers.rs +++ b/src/syscall/microkernel/numbers.rs @@ -77,3 +77,7 @@ pub const SYS_PCI_CONFIG_WRITE: u64 = tag4(b"MPCW"); // Spawn another window instance of an embedded, attested app capsule // (terminal or browser). Gated on the SpawnWindow capability. pub const SYS_SPAWN_INSTANCE: u64 = tag4(b"MSPI"); + +// Run a baked, attested command-line tool by name, parented to the caller so +// it can drive the tool's stdin and stdout. Gated on the IPC capability. +pub const SYS_TOOL_RUN: u64 = tag4(b"MTRN"); diff --git a/src/syscall/microkernel/tool_run.rs b/src/syscall/microkernel/tool_run.rs new file mode 100644 index 0000000000..0656eb12fd --- /dev/null +++ b/src/syscall/microkernel/tool_run.rs @@ -0,0 +1,58 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use super::errnos::{ERRNO_FAULT, ERRNO_INVAL, ERRNO_NOENT}; +use crate::usercopy::{read_user_bytes, validate_user_read}; + +const MAX_NAME: usize = 48; +const MAX_ARGV: usize = 4096; + +/// `MkToolRun(name_ptr, name_len, argv_ptr, argv_len)`. Run one of the baked, +/// attested command-line tools by its service name (for example `tool.tokei`), +/// parented to the caller so the caller feeds its stdin and drains its stdout +/// through the existing `MkProcInput`/`MkProcOutput` path. `argv` is the tool's +/// NUL-separated argument blob (`name\0arg1\0...`), empty for none. Returns the +/// new tool's pid, or an errno. Only the baked set can be named, so a caller can +/// never point this at an arbitrary binary. +pub fn sys_tool_run(name_ptr: u64, name_len: u64, argv_ptr: u64, argv_len: u64) -> i64 { + let nlen = name_len as usize; + if nlen == 0 || nlen > MAX_NAME || validate_user_read(name_ptr, nlen).is_err() { + return ERRNO_INVAL; + } + let name = match read_user_bytes(name_ptr, nlen) { + Ok(b) => b, + Err(_) => return ERRNO_FAULT, + }; + let alen = argv_len as usize; + if alen > MAX_ARGV { + return ERRNO_INVAL; + } + let argv = if alen == 0 { + alloc::vec::Vec::new() + } else { + if validate_user_read(argv_ptr, alen).is_err() { + return ERRNO_INVAL; + } + match read_user_bytes(argv_ptr, alen) { + Ok(b) => b, + Err(_) => return ERRNO_FAULT, + } + }; + match crate::userspace::tool_capsules::run_named(&name, &argv) { + Some(pid) => pid as i64, + None => ERRNO_NOENT, + } +} diff --git a/src/syscall/numbers/defs.rs b/src/syscall/numbers/defs.rs index 089072ebd9..74ccf38300 100644 --- a/src/syscall/numbers/defs.rs +++ b/src/syscall/numbers/defs.rs @@ -74,6 +74,7 @@ pub enum SyscallNumber { MkProcInput = tag4(b"MPIN"), MkStdinRead = tag4(b"MSRD"), MkAttestStatus = tag4(b"MAST"), + MkToolRun = tag4(b"MTRN"), MkCapGrant = tag4(b"MCGT"), MkCapRevoke = tag4(b"MCRV"), MkCapCheck = tag4(b"MCCK"), diff --git a/src/userspace/init/entry.rs b/src/userspace/init/entry.rs index 9ddfbd3444..4c86ad9f7c 100644 --- a/src/userspace/init/entry.rs +++ b/src/userspace/init/entry.rs @@ -34,6 +34,7 @@ pub fn run_init() -> ! { spawn_plan::spawn_apps(); run_tokio_smoke(); boot_log::ok("INIT", "Capsules spawned"); + run_tool_selftest(); lower_init_priority(); yield_after_spawns(); launch_final_payload(); @@ -81,6 +82,39 @@ fn run_sd() { #[cfg(not(feature = "nonos-capsule-sd"))] fn run_sd() {} +// Boot self-test: run each installed tool once with an argument that prints and +// exits without reading stdin (`--version`, or `-h` for the getopts tool). The +// tool is spawned parented to init, so its stdout flows through the debug sink +// onto the serial log, proving the whole run path end to end. A marker frames +// each tool's output so the boot log can be asserted against. +#[cfg(feature = "nonos-tool-selftest")] +fn run_tool_selftest() { + const TESTS: &[(&[u8], &[u8], &str)] = &[ + (b"tool.grex", b"grex\0--version", "grex"), + (b"tool.tokei", b"tokei\0--version", "tokei"), + (b"tool.csview", b"csview\0--version", "csview"), + (b"tool.pastel", b"pastel\0--version", "pastel"), + (b"tool.jsonxf", b"jsonxf\0-h", "jsonxf"), + (b"tool.huniq", b"huniq\0--version", "huniq"), + (b"tool.dotenv-linter", b"dotenv-linter\0--version", "dotenv-linter"), + ]; + for (service, argv, label) in TESTS { + boot_log::ok("TOOL-SELFTEST run", label); + if crate::userspace::tool_capsules::run_named(service, argv).is_none() { + boot_log::error("tool self-test spawn failed"); + } + // Let the scheduler run the tool to completion before the next one, so + // its output is not interleaved and the port is free again. + for _ in 0..4000 { + crate::sched::yield_now(); + } + boot_log::ok("TOOL-SELFTEST done", label); + } +} + +#[cfg(not(feature = "nonos-tool-selftest"))] +fn run_tool_selftest() {} + #[cfg(feature = "nonos-capsule-tokio-smoke")] fn run_tokio_smoke() { match crate::userspace::capsule_tokio_smoke::spawn_tokio_smoke_capsule() { diff --git a/src/userspace/mod.rs b/src/userspace/mod.rs index b96f591ca9..1e0c37bf9e 100644 --- a/src/userspace/mod.rs +++ b/src/userspace/mod.rs @@ -79,5 +79,6 @@ pub mod capsule_wallpaper; pub mod capsule_wallpaper_catalog; pub mod capsule_wm; pub mod init; +pub mod tool_capsules; pub use init::run_init; diff --git a/src/userspace/tool_capsules/embed_macro.rs b/src/userspace/tool_capsules/embed_macro.rs new file mode 100644 index 0000000000..002e3c8b34 --- /dev/null +++ b/src/userspace/tool_capsules/embed_macro.rs @@ -0,0 +1,36 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! The `tool_capsule!` macro: bake one tool's signed artifacts into the image. + +/// Build a `ToolCapsule` from its endpoint identity and its binary name. The ELF +/// path is given explicitly; the cert, manifest, and attestation trailer are +/// found by binary name under the trust directory. All paths resolve relative to +/// the invoking file, which is the registry. +macro_rules! tool_capsule { + ($name:literal, $port:expr, $reply:literal, $reply_port:expr, $elf:literal, $bin:literal) => { + super::spec::ToolCapsule { + name: $name, + service_port: $port, + reply_inbox: $reply, + reply_port: $reply_port, + elf: include_bytes!($elf), + cert: include_bytes!(concat!( + "../../../nonos-data/trust/capsules/", + $bin, + ".nonos_id_cert.bin" + )), + manifest: include_bytes!(concat!( + "../../../nonos-data/trust/capsules/", + $bin, + ".manifest.bin" + )), + attestation: include_bytes!(concat!( + "../../../nonos-data/trust/capsules/", + $bin, + ".zk_trailer.bin" + )), + } + }; +} diff --git a/src/userspace/tool_capsules/mod.rs b/src/userspace/tool_capsules/mod.rs new file mode 100644 index 0000000000..d2fbfde391 --- /dev/null +++ b/src/userspace/tool_capsules/mod.rs @@ -0,0 +1,17 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Tool capsules are signed, STARK-attested crates.io utilities baked into the +//! kernel image from `userland/apps.list`. Each carries its ELF, NONOS-ID cert, +//! manifest, and STARK trailer; the spawner verifies all four under the baked +//! trust anchor before it runs. Tools run on demand through `run_named`, not at +//! boot: a command-line tool has nothing to do until it is invoked. + +#[cfg(feature = "nonos-tool-capsules")] +#[macro_use] +mod embed_macro; +mod registry; +mod spec; + +pub use registry::run_named; diff --git a/src/userspace/tool_capsules/registry.rs b/src/userspace/tool_capsules/registry.rs new file mode 100644 index 0000000000..c82ebfd1b1 --- /dev/null +++ b/src/userspace/tool_capsules/registry.rs @@ -0,0 +1,62 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! The list of embedded tool capsules. The block between the generated markers +//! is written by `tools/nonos-app` from `userland/apps.list`, the single +//! source of truth. Adding a tool is `nonos-app add `, not a hand edit. + +extern crate alloc; + +use alloc::vec; +use alloc::vec::Vec; + +use super::spec::ToolCapsule; +use crate::sys::boot_log; + +/// Every embedded tool capsule, generated from `userland/apps.list`. Off the +/// `nonos-tool-capsules` feature (core builds that do not cross-compile the tool +/// binaries) the list is empty, so nothing is `include_bytes`d. +#[cfg(not(feature = "nonos-tool-capsules"))] +fn embedded_tools() -> Vec { + Vec::new() +} + +#[cfg(feature = "nonos-tool-capsules")] +fn embedded_tools() -> Vec { + vec![ + // nonos-app:begin (generated; do not edit by hand) + tool_capsule!("tool.grex", 4900, "endpoint.tool.grex.reply", 4901, + "../../../target/upstream-grex/bin/grex", "grex"), + tool_capsule!("tool.dotenv-linter", 4902, "endpoint.tool.dotenv-linter.reply", 4903, + "../../../target/upstream-dotenv-linter/bin/dotenv-linter", "dotenv-linter"), + tool_capsule!("tool.pastel", 4904, "endpoint.tool.pastel.reply", 4905, + "../../../target/upstream-pastel/bin/pastel", "pastel"), + tool_capsule!("tool.jsonxf", 4906, "endpoint.tool.jsonxf.reply", 4907, + "../../../target/upstream-jsonxf/bin/jsonxf", "jsonxf"), + tool_capsule!("tool.tokei", 4910, "endpoint.tool.tokei.reply", 4911, + "../../../target/upstream-tokei/bin/tokei", "tokei"), + tool_capsule!("tool.huniq", 4912, "endpoint.tool.huniq.reply", 4913, + "../../../target/upstream-huniq/bin/huniq", "huniq"), + tool_capsule!("tool.csview", 4914, "endpoint.tool.csview.reply", 4915, + "../../../target/upstream-csview/bin/csview", "csview"), + // nonos-app:end + ] +} + +/// Run the embedded tool whose service name matches `name` (for example +/// `tool.tokei`), verified under the baked trust anchor and parented to the +/// caller so it can drive the tool's stdin and stdout. `argv` is the NUL +/// separated argument blob. Returns the tool's pid, or `None` when no tool +/// matches or the spawn is rejected. A command-line tool has nothing to do +/// until invoked, so tools are launched here on demand, not at boot. +pub fn run_named(name: &[u8], argv: &[u8]) -> Option { + let tool = embedded_tools().into_iter().find(|t| t.name.as_bytes() == name)?; + match tool.spawn_with_args(argv) { + Ok(pid) => Some(pid), + Err(_) => { + boot_log::error("tool capsule spawn failed"); + None + } + } +} diff --git a/src/userspace/tool_capsules/spec.rs b/src/userspace/tool_capsules/spec.rs new file mode 100644 index 0000000000..96c7266a21 --- /dev/null +++ b/src/userspace/tool_capsules/spec.rs @@ -0,0 +1,65 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! One embedded tool capsule and how it spawns. The identity and the signed, +//! attested artifacts are data; the capability set and trust anchor are the same +//! for every tool, so a tool is a value, not a hand-written module. + +extern crate alloc; + +use crate::capabilities::Capability; +use crate::kernel_core::process_spawn::capsule_spawn::{self, CapsuleSpecVerified, SpawnError}; +use crate::security::nonos_id_cert::IdCertVerifyError; +use crate::security::nonos_trust_anchor::{decode as decode_trust_anchor, BAKED_TRUST_ANCHOR_POLICY}; + +/// A tool capsule baked into the kernel image: its endpoint identity and the +/// signed cert, manifest, and STARK attestation trailer the spawner verifies. +pub struct ToolCapsule { + pub name: &'static str, + pub service_port: u32, + pub reply_inbox: &'static str, + pub reply_port: u32, + pub elf: &'static [u8], + pub cert: &'static [u8], + pub manifest: &'static [u8], + pub attestation: &'static [u8], +} + +impl ToolCapsule { + /// Verify the artifacts under the baked trust anchor and spawn the tool with + /// the sandboxed capability set (execute, IPC, memory) every tool shares. + /// The process is parented to the caller in the syscall context, so that + /// caller feeds its stdin and drains its stdout, and `argv` (a NUL-separated + /// `name\0arg1\0...` blob) becomes the tool's argument vector. Returns the + /// tool's pid. + pub fn spawn_with_args(&self, argv: &[u8]) -> Result { + let trust_anchor = decode_trust_anchor(BAKED_TRUST_ANCHOR_POLICY) + .map_err(|_| SpawnError::NonosIdCertRejected(IdCertVerifyError::TrustAnchorPolicy))?; + let spec = CapsuleSpecVerified { + name: self.name, + service_port: self.service_port, + reply_inbox: self.reply_inbox, + reply_port: self.reply_port, + elf: self.elf, + nonos_id_cert_bytes: self.cert, + manifest_bytes: self.manifest, + attestation_trailer: self.attestation, + target_triple: "x86_64-nonos-user", + requested_caps: Capability::CoreExec.bit() + | Capability::IPC.bit() + | Capability::Memory.bit(), + debug_tag: b"", + }; + let pid = capsule_spawn::spawn_verified(&spec, &trust_anchor, None)?; + if !argv.is_empty() { + let vec: alloc::vec::Vec = argv + .split(|&b| b == 0) + .filter(|s| !s.is_empty()) + .map(|s| alloc::string::String::from_utf8_lossy(s).into_owned()) + .collect(); + crate::process::with_process(pid, |pcb| *pcb.argv.lock() = vec); + } + Ok(pid) + } +} diff --git a/tools/nonos-app b/tools/nonos-app new file mode 100755 index 0000000000..24080fb311 --- /dev/null +++ b/tools/nonos-app @@ -0,0 +1,299 @@ +#!/usr/bin/env bash +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# nonos-app: install a crates.io tool as a signed, STARK-attested NONOS capsule +# from one command. It cross-compiles the crate for x86_64-nonos, mints its +# publisher keys, signs it, records it in userland/apps.list (the single source +# of truth), and regenerates the kernel tool registry. No kernel file is edited +# by hand. After `add`, run `make nonos-mk-desktop-gui-prod` to enroll + build. +# +# nonos-app add [bin] install a crate (bin defaults to the crate name) +# nonos-app regen rewrite the registry block from apps.list +# nonos-app list show installed tool apps +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +APPS="$ROOT/userland/apps.list" +REGISTRY="$ROOT/src/userspace/tool_capsules/registry.rs" +BUILDMK="$ROOT/mk/20-build.mk" +TARGET="$ROOT/userland/x86_64-nonos-user.json" +RTOBJ="$ROOT/target/nonos_rt.o" +TC="nightly-2026-01-16" +NB="$(dirname "$(RUSTUP_TOOLCHAIN=$TC rustup which rustc)")" +CS="$ROOT/nonos-sign/target/release/capsule-sign" + +die() { echo "nonos-app: $*" >&2; exit 1; } + +# The highest service port currently in apps.list, or 4898 so the first app +# starts at 4900. The system capsules (desktop apps, compositor, drivers) hold +# ports up to 4865, so tools begin above that range to avoid endpoint clashes. +next_ports() { + local max=4898 + while read -r _ _ sp _; do [ -n "${sp:-}" ] && [ "$sp" -gt "$max" ] && max=$sp; done \ + < <(grep -vE '^\s*#|^\s*$' "$APPS" 2>/dev/null || true) + echo $((max + 2)) $((max + 3)) +} + +CC_FLAGS="-Zbuild-std=std,panic_abort -Zbuild-std-features=compiler-builtins-mem" + +# The vendored NONOS shim dir under upstream-src for a foundational crate that +# assumes unix, or empty if there is no shim for it yet. +shim_dir_for() { + case "$1" in + ctrlc) echo "ctrlc-3.5.2" ;; + getrandom) echo "getrandom-0.2.17" ;; + atty) echo "atty-0.2.14" ;; + home) echo "home-0.5.12" ;; + fd-lock) echo "fd-lock-4.0.4" ;; + dirs) echo "dirs-6.0.0" ;; + os_str_bytes) echo "os_str_bytes-6.6.1" ;; + errno) echo "errno-0.3.14" ;; + is-terminal) echo "is-terminal-0.4.17" ;; + *) echo "" ;; + esac +} + +# Copy a crate's fetched source from the registry cache into upstream-src so it +# can be built with --path and local [patch] overrides. +vendor_app() { + local crate="$1" src + # Match only -, never a sibling like -core, by + # requiring a digit right after the crate name. + src="$(ls -d "$HOME/.cargo/registry/src"/*/"$crate"-[0-9]* 2>/dev/null | sort -V | tail -1)" + [ -n "$src" ] || die "no fetched source for $crate in the registry cache" + rm -rf "$ROOT/userland/upstream-src/$crate" + cp -R "$src" "$ROOT/userland/upstream-src/$crate" + chmod -R u+w "$ROOT/userland/upstream-src/$crate" +} + +# Rewrite the [patch.crates-io] block in the vendored manifest for every shim +# accumulated so far, replacing any previous block. +write_patches() { + local crate="$1"; shift + local mk="$ROOT/userland/upstream-src/$crate/Cargo.toml" s + sed -i '' '/# nonos-app-patch/,$d' "$mk" + { printf '\n# nonos-app-patch\n[patch.crates-io]\n' + for s in "$@"; do printf '%s = { path = "../%s" }\n' "$s" "$(shim_dir_for "$s")"; done + } >> "$mk" +} + +# One build attempt (remote crate or vendored --path), log captured for blocker +# detection. Args: [extra cargo args, e.g. --features]. +build_once() { + local tgt="$1" log="$2" bin="$3"; shift 3 + PATH="$NB:$PATH" RUSTUP_TOOLCHAIN="$TC" \ + RUSTFLAGS="-Clink-arg=$RTOBJ --cfg getrandom_backend=\"rdrand\"" \ + CARGO_TARGET_DIR="$ROOT/target/nonos-app-target" \ + "$NB/cargo" install $tgt --no-default-features --target "$TARGET" $CC_FLAGS "$@" \ + --root "$ROOT/target/upstream-$bin" --no-track --force --bin "$bin" > "$log" 2>&1 +} + +# Cross-compile for x86_64-nonos, applying NONOS shims for known foundational +# crates and retrying until it builds or hits a blocker with no shim. +cross_compile() { + local crate="$1" bin="$2" log vendored="" attempt; log="$(mktemp)" + local shims=() feats=() feat_arg + if [ -f "$ROOT/target/upstream-$bin/bin/$bin" ]; then + echo " reusing prebuilt $bin"; return + fi + echo " cross-compiling $crate (bin $bin)..." + for attempt in 1 2 3 4 5 6 7 8; do + feat_arg="" + [ ${#feats[@]} -gt 0 ] && feat_arg="--features=$(IFS=,; echo "${feats[*]}")" + if [ -z "$vendored" ]; then + build_once "$crate" "$log" "$bin" $feat_arg && { echo " built clean (${feats[*]:-no extra features})"; return; } + else + write_patches "$crate" "${shims[@]}" + build_once "--path $ROOT/userland/upstream-src/$crate" "$log" "$bin" $feat_arg \ + && { echo " built with shims: ${shims[*]:-}"; return; } + fi + # A crate whose binary is gated behind a feature (commonly `cli`): enable it + # and retry rather than treat the missing bin as a dead end. + local need; need="$(grep -oE "requires the features: .*" "$log" | head -1 | grep -oE '\`[^\`]+\`' | tr -d '\`' | tr '\n' ' ')" || true + if [ -n "$need" ]; then + feats+=($need); echo " enabling features: $need, retrying..."; continue + fi + local blocker; blocker="$(grep -oE "could not compile \`[^\`]+\`" "$log" | head -1 | sed -E 's/.*`([^`]+)`.*/\1/')" || true + [ -n "$(shim_dir_for "$blocker")" ] \ + || die "$crate needs '${blocker:-unknown}', which has no NONOS shim yet (or is a hard C/GUI/net/spawn dep). Add a shim under userland/upstream-src and a shim_dir_for entry." + # If a shim is already applied yet the same crate still fails, the shim does + # not cover this crate's version. Stop rather than loop applying it forever. + case " ${shims[*]:-} " in + *" $blocker "*) die "$crate still fails on '$blocker' after its shim; the shim likely targets a different version." ;; + esac + [ -z "$vendored" ] && { vendor_app "$crate"; vendored=1; } + shims+=("$blocker") + echo " applying $blocker shim, retrying..." + done + die "$crate did not converge after shims: ${shims[*]:-} features: ${feats[*]:-}" +} + +# Mint the hybrid ed25519 + ML-DSA-65 publisher keypair and publish the pubs. +gen_keys() { + local bin="$1" alg + for alg in ed25519 mldsa65; do + "$CS" keygen --alg "$alg" --out "$ROOT/.keys/${bin}_publisher_$alg" >/dev/null + cp "$ROOT/.keys/${bin}_publisher_$alg.pub" "$ROOT/nonos-data/trust/keys/" + done +} + +# Write the capsule manifest fragment that names the endpoint and points at the +# cross-compiled binary. +gen_capsule_mk() { + local slug="$1" bin="$2" sp="$3" rp="$4" crate="$5" + mkdir -p "$ROOT/userland/capsule_$slug" + cat > "$ROOT/userland/capsule_$slug/Capsule.mk" < "$REGISTRY" <\`, not a hand edit. + +extern crate alloc; + +use alloc::vec; +use alloc::vec::Vec; + +use super::spec::ToolCapsule; +use crate::sys::boot_log; + +/// Every embedded tool capsule, generated from \`userland/apps.list\`. Off the +/// \`nonos-tool-capsules\` feature (core builds that do not cross-compile the +/// tool binaries) the list is empty, so nothing is \`include_bytes\`d. +#[cfg(not(feature = "nonos-tool-capsules"))] +fn embedded_tools() -> Vec { + Vec::new() +} + +#[cfg(feature = "nonos-tool-capsules")] +fn embedded_tools() -> Vec { + vec![ + // nonos-app:begin (generated; do not edit by hand) +$body // nonos-app:end + ] +} + +/// Run the embedded tool whose service name matches \`name\`, parented to the +/// caller so it can drive the tool's stdin and stdout. \`argv\` is the NUL +/// separated argument blob. Returns the tool's pid, or \`None\`. Tools run on +/// demand, not at boot: a command-line tool has nothing to do until invoked. +pub fn run_named(name: &[u8], argv: &[u8]) -> Option { + let tool = embedded_tools().into_iter().find(|t| t.name.as_bytes() == name)?; + match tool.spawn_with_args(argv) { + Ok(pid) => Some(pid), + Err(_) => { + boot_log::error("tool capsule spawn failed"); + None + } + } +} +EOF + echo " registry regenerated from apps.list" +} + +# Regenerate the desktop shell's tool app table so the Launchpad shows one tile +# per installed tool. The shell is a separate capsule and cannot include the +# kernel registry, so it gets its own generated table from the same apps.list. +regen_tool_apps() { + local table="$ROOT/userland/capsule_desktop_shell/src/state/tool_apps.rs" body="" slug bin sp rp n=0 + [ -d "$(dirname "$table")" ] || return 0 + while read -r slug bin sp rp; do + [ -z "${slug:-}" ] && continue + body+=" ToolApp { label: b\"$slug\", service: b\"tool.$slug\" },"$'\n' + n=$((n + 1)) + done < <(grep -vE '^\s*#|^\s*$' "$APPS") + cat > "$table" <\`. + +/// One installed tool: the name on its tile and the service it launches under. +pub struct ToolApp { + pub label: &'static [u8], + pub service: &'static [u8], +} + +/// Every installed tool, generated from \`userland/apps.list\`. +pub const TOOL_APPS: [ToolApp; $n] = [ +$body]; +EOF + echo " desktop tool_apps table regenerated ($n tools)" +} + +cmd_add() { + local crate="$1" bin="${2:-$1}" slug="${2:-$1}" + grep -qE "^$slug " "$APPS" && die "$slug is already installed" + read -r sp rp < <(next_ports) + cross_compile "$crate" "$bin" + gen_keys "$bin" + gen_capsule_mk "$slug" "$bin" "$sp" "$rp" "$crate" + wire_build "$slug" + printf '%s %s %s %s\n' "$slug" "$bin" "$sp" "$rp" >> "$APPS" + regen_registry + regen_tool_apps + echo "installed $slug (ports $sp/$rp). Run: make nonos-mk-desktop-gui-prod NONOS_DEV=1" +} + +cmd_list() { grep -vE '^\s*#|^\s*$' "$APPS" | awk '{printf " %-16s %s:%s\n",$1,$3,$4}'; } + +case "${1:-}" in + add) shift; [ $# -ge 1 ] || die "usage: nonos-app add [bin]"; cmd_add "$@" ;; + regen) regen_registry; regen_tool_apps ;; + list) cmd_list ;; + *) die "usage: nonos-app {add [bin] | regen | list}" ;; +esac diff --git a/userland/apps.list b/userland/apps.list new file mode 100644 index 0000000000..b01707d758 --- /dev/null +++ b/userland/apps.list @@ -0,0 +1,10 @@ +# NONOS installed tool apps. One line per app: slug bin service_port reply_port. +# This is the single source of truth. `tools/nonos-app add ` appends here +# and regenerates the kernel registry; nothing else is hand-edited. +grex grex 4900 4901 +dotenv-linter dotenv-linter 4902 4903 +pastel pastel 4904 4905 +jsonxf jsonxf 4906 4907 +tokei tokei 4910 4911 +huniq huniq 4912 4913 +csview csview 4914 4915 diff --git a/userland/capsule_csview/Capsule.mk b/userland/capsule_csview/Capsule.mk new file mode 100644 index 0000000000..48d28b7639 --- /dev/null +++ b/userland/capsule_csview/Capsule.mk @@ -0,0 +1,21 @@ +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# csview: unmodified crates.io source cross-compiled for x86_64-nonos and +# installed as a signed, STARK-attested capsule. Generated by tools/nonos-app. + +CAPSULE_SLUG := csview +CAPSULE_HANDLE := csview +CAPSULE_DIR := userland/capsule_csview +CAPSULE_BIN_NAME := csview +CAPSULE_DOMAIN := crates.io +CAPSULE_NAMESPACE := systems.nonos.tool.csview +CAPSULE_SERVICE_ENDPOINT := service:4914:tool.csview +CAPSULE_REPLY_ENDPOINT := reply:4915:endpoint.tool.csview.reply +CAPSULE_REQUIRED_CAPS := 0x19 +CAPSULE_CAPS_CEILING := 0x19 +CAPSULE_PREBUILT_BIN := target/upstream-csview/bin/csview +CAPSULE_METADATA := crates.io csview publisher + +include nonos-mk/capsule.mk diff --git a/userland/capsule_desktop_shell/assets/app_icons/tools/choose.rgba b/userland/capsule_desktop_shell/assets/app_icons/tools/choose.rgba new file mode 100644 index 0000000000..d28f0c48df Binary files /dev/null and b/userland/capsule_desktop_shell/assets/app_icons/tools/choose.rgba differ diff --git a/userland/capsule_desktop_shell/assets/app_icons/tools/csview.rgba b/userland/capsule_desktop_shell/assets/app_icons/tools/csview.rgba new file mode 100644 index 0000000000..5b1cd7b4a6 Binary files /dev/null and b/userland/capsule_desktop_shell/assets/app_icons/tools/csview.rgba differ diff --git a/userland/capsule_desktop_shell/assets/app_icons/tools/dotenv_linter.rgba b/userland/capsule_desktop_shell/assets/app_icons/tools/dotenv_linter.rgba new file mode 100644 index 0000000000..f2e71e6f4f Binary files /dev/null and b/userland/capsule_desktop_shell/assets/app_icons/tools/dotenv_linter.rgba differ diff --git a/userland/capsule_desktop_shell/assets/app_icons/tools/grex.rgba b/userland/capsule_desktop_shell/assets/app_icons/tools/grex.rgba new file mode 100644 index 0000000000..8db99c1eea Binary files /dev/null and b/userland/capsule_desktop_shell/assets/app_icons/tools/grex.rgba differ diff --git a/userland/capsule_desktop_shell/assets/app_icons/tools/huniq.rgba b/userland/capsule_desktop_shell/assets/app_icons/tools/huniq.rgba new file mode 100644 index 0000000000..c9237912fe Binary files /dev/null and b/userland/capsule_desktop_shell/assets/app_icons/tools/huniq.rgba differ diff --git a/userland/capsule_desktop_shell/assets/app_icons/tools/jsonxf.rgba b/userland/capsule_desktop_shell/assets/app_icons/tools/jsonxf.rgba new file mode 100644 index 0000000000..963ae4f2f7 Binary files /dev/null and b/userland/capsule_desktop_shell/assets/app_icons/tools/jsonxf.rgba differ diff --git a/userland/capsule_desktop_shell/assets/app_icons/tools/pastel.rgba b/userland/capsule_desktop_shell/assets/app_icons/tools/pastel.rgba new file mode 100644 index 0000000000..658bfb88b1 Binary files /dev/null and b/userland/capsule_desktop_shell/assets/app_icons/tools/pastel.rgba differ diff --git a/userland/capsule_desktop_shell/assets/app_icons/tools/tokei.rgba b/userland/capsule_desktop_shell/assets/app_icons/tools/tokei.rgba new file mode 100644 index 0000000000..6451b14ac8 Binary files /dev/null and b/userland/capsule_desktop_shell/assets/app_icons/tools/tokei.rgba differ diff --git a/userland/capsule_desktop_shell/src/render/bottom_taskbar.rs b/userland/capsule_desktop_shell/src/render/bottom_taskbar.rs index 26accb507c..e04c3ef253 100644 --- a/userland/capsule_desktop_shell/src/render/bottom_taskbar.rs +++ b/userland/capsule_desktop_shell/src/render/bottom_taskbar.rs @@ -16,10 +16,28 @@ use super::draw_app_icon; use super::fill::fill_rect; -use super::layout::{bottom_dock_rect, TASKBAR_ENTRY_W}; +use super::layout::{bottom_dock_rect, launchpad_slot_x, TASKBAR_ENTRY_W}; use crate::state::{Context, LAUNCHER_APPS, TASKBAR_NO_ACTIVE}; const ICON_SIZE: u32 = 40; +const LAUNCHPAD_DOT: u32 = 0xFF9F_B4D6; + +// The Launchpad button: the familiar 3x3 grid of cells, sitting in the dock +// slot just past the last app. +fn draw_launchpad_button(ctx: &Context, box_top: u32, box_h: u32) { + let slot_x = launchpad_slot_x(bottom_dock_rect(ctx.width, ctx.height)); + let x0 = slot_x + (TASKBAR_ENTRY_W - ICON_SIZE) / 2; + let y0 = box_top + (box_h - ICON_SIZE) / 2; + let cell = ICON_SIZE / 3; + let dot = cell.saturating_sub(4); + for row in 0..3 { + for col in 0..3 { + let x = x0 + col * cell + 2; + let y = y0 + row * cell + 2; + fill_rect(ctx.backing_va, ctx.stride, ctx.width, ctx.height, x, y, dot, dot, LAUNCHPAD_DOT); + } + } +} pub fn paint_bottom_taskbar(ctx: &Context) { let dock = bottom_dock_rect(ctx.width, ctx.height); @@ -70,4 +88,5 @@ pub fn paint_bottom_taskbar(ctx: &Context) { draw_app_icon(ctx, icon_x, icon_y, app.icon, ICON_SIZE); x += TASKBAR_ENTRY_W + 6; } + draw_launchpad_button(ctx, box_top, box_h); } diff --git a/userland/capsule_desktop_shell/src/render/chrome.rs b/userland/capsule_desktop_shell/src/render/chrome.rs index 11ef0971fb..bdb9974c01 100644 --- a/userland/capsule_desktop_shell/src/render/chrome.rs +++ b/userland/capsule_desktop_shell/src/render/chrome.rs @@ -43,6 +43,10 @@ pub fn paint_chrome(ctx: &Context) { if ctx.spotlight.visible { paint_rect::paint_rect(ctx, spotlight_rect(ctx.width, ctx.height), SPOTLIGHT_ARGB); } + // The Launchpad, when open, covers the whole desktop and its dock. + if ctx.launchpad { + super::launchpad::paint_launchpad(ctx); + } // The right-click menu floats above everything else, dock included. super::desktop_menu::paint(ctx); } diff --git a/userland/capsule_desktop_shell/src/render/icons.rs b/userland/capsule_desktop_shell/src/render/icons.rs index 561b85a8f7..71d1a4ce96 100644 --- a/userland/capsule_desktop_shell/src/render/icons.rs +++ b/userland/capsule_desktop_shell/src/render/icons.rs @@ -65,6 +65,12 @@ pub fn draw_fs_icon(ctx: &Context, x: u32, y: u32, size: u32, is_dir: bool) { badge::badge(ctx, x, y, size, glyph, CYAN); } +/// An installed-tool tile from its own 48x48 glyph mask, in the same badge +/// and cyan tile language as the desktop apps. +pub fn draw_tool_icon(ctx: &Context, x: u32, y: u32, size: u32, glyph: &'static [u8]) { + badge::badge(ctx, x, y, size, glyph, CYAN); +} + /// The real NØNOS logo, drawn as the top-left brand mark on the menu bar. pub fn draw_logo(ctx: &Context, x: u32, y: u32, size: u32) { nonos_logo::paint(ctx, x, y, size, 0); diff --git a/userland/capsule_desktop_shell/src/render/launchpad/gen_icon.rs b/userland/capsule_desktop_shell/src/render/launchpad/gen_icon.rs new file mode 100644 index 0000000000..6dc834785b --- /dev/null +++ b/userland/capsule_desktop_shell/src/render/launchpad/gen_icon.rs @@ -0,0 +1,51 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! A generated tile for a command-line tool that ships no artwork: a coloured +//! rounded square, its hue derived from the name so each tool stays visually +//! distinct, with the tool's uppercase initial in the centre. + +use crate::render::fill::fill_rect; +use crate::render::text::draw_overlay_text; +use crate::state::Context; + +const TILE_BG: u32 = 0xFF0C_1016; +const GLYPH_ADV: u32 = 8; + +pub(super) fn draw(ctx: &Context, x: u32, y: u32, size: u32, label: &[u8]) { + let (va, st, vw, vh) = (ctx.backing_va, ctx.stride, ctx.width, ctx.height); + fill_rect(va, st, vw, vh, x, y, size, size, TILE_BG); + let accent = hue(label); + fill_rect(va, st, vw, vh, x + 4, y + 4, size - 8, size - 8, accent); + // Knock a small stair off each corner so the fill reads as rounded. + let s2 = size.saturating_sub(6); + for &(cx, cy) in &[(x + 4, y + 4), (x + s2, y + 4), (x + 4, y + s2), (x + s2, y + s2)] { + fill_rect(va, st, vw, vh, cx, cy, 2, 2, TILE_BG); + } + let ch = [initial(label)]; + let gx = x + size / 2 - GLYPH_ADV / 2; + let gy = y + size / 2 - 6; + draw_overlay_text(ctx, gx, gy, &ch, 0xFF0A_0E14); +} + +// The tool's first letter, uppercased for the tile glyph. +fn initial(label: &[u8]) -> u8 { + match label.first().copied() { + Some(c @ b'a'..=b'z') => c - 32, + Some(c) => c, + None => b'?', + } +} + +// A stable, bright hue from the name so distinct tools never share a colour. +fn hue(label: &[u8]) -> u32 { + let mut h: u32 = 2166136261; + for &b in label { + h = (h ^ b as u32).wrapping_mul(16777619); + } + let r = 0x60 | (h & 0x7F); + let g = 0x60 | ((h >> 8) & 0x7F); + let b = 0x60 | ((h >> 16) & 0x7F); + 0xFF00_0000 | (r << 16) | (g << 8) | b +} diff --git a/userland/capsule_desktop_shell/src/render/launchpad/grid.rs b/userland/capsule_desktop_shell/src/render/launchpad/grid.rs new file mode 100644 index 0000000000..1fcce3b544 --- /dev/null +++ b/userland/capsule_desktop_shell/src/render/launchpad/grid.rs @@ -0,0 +1,33 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Grid geometry for the Launchpad: a centred, row-major grid holding every +//! desktop app followed by every installed tool. + +use crate::state::{LAUNCHER_APPS, TOOL_APPS}; + +pub(super) const TILE: u32 = 72; +pub(super) const CELL_W: u32 = 120; +pub(super) const CELL_H: u32 = 108; +pub(super) const TOP_PAD: u32 = 110; + +/// Total tiles: the desktop apps first, then the installed command-line tools. +pub(super) fn count() -> usize { + LAUNCHER_APPS.len() + TOOL_APPS.len() +} + +/// Columns that fit the display, kept within a sensible range so the grid stays +/// centred rather than stretching edge to edge on a wide screen. +pub(super) fn cols(width: u32) -> u32 { + (width / CELL_W).clamp(1, 8) +} + +/// Top-left screen position of the nth cell. +pub(super) fn cell_origin(width: u32, index: usize) -> (u32, u32) { + let c = cols(width); + let i = index as u32; + let grid_w = c * CELL_W; + let left = width.saturating_sub(grid_w) / 2; + (left + (i % c) * CELL_W, TOP_PAD + (i / c) * CELL_H) +} diff --git a/userland/capsule_desktop_shell/src/render/launchpad/hit.rs b/userland/capsule_desktop_shell/src/render/launchpad/hit.rs new file mode 100644 index 0000000000..7cd1cb21a4 --- /dev/null +++ b/userland/capsule_desktop_shell/src/render/launchpad/hit.rs @@ -0,0 +1,33 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Which Launchpad tile a point falls on, and what it launches. + +use super::grid::{cell_origin, count, CELL_H, CELL_W}; +use crate::state::LAUNCHER_APPS; + +/// What a clicked tile launches: a desktop app by index, or an installed tool +/// by index into the tool table. +pub enum Target { + App(usize), + Tool(usize), +} + +/// Index of the tile under a point, if any. +pub fn hit(width: u32, px: u32, py: u32) -> Option { + (0..count()).find(|&i| { + let (x, y) = cell_origin(width, i); + px >= x && px < x + CELL_W && py >= y && py < y + CELL_H + }) +} + +/// Resolve a tile index to the app or tool it stands for. +pub fn target(index: usize) -> Target { + let apps = LAUNCHER_APPS.len(); + if index < apps { + Target::App(index) + } else { + Target::Tool(index - apps) + } +} diff --git a/userland/capsule_desktop_shell/src/render/launchpad/mod.rs b/userland/capsule_desktop_shell/src/render/launchpad/mod.rs new file mode 100644 index 0000000000..863a33b624 --- /dev/null +++ b/userland/capsule_desktop_shell/src/render/launchpad/mod.rs @@ -0,0 +1,17 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! The Launchpad: a full-screen grid of every desktop app and installed tool, +//! opened from the dock. Drawing and hit-testing live here; the click action +//! that launches a tile and dismisses the overlay lives in the server layer. + +mod gen_icon; +mod grid; +mod hit; +mod paint; +mod tile; +mod tool_icons; + +pub use hit::{hit, target, Target}; +pub use paint::paint_launchpad; diff --git a/userland/capsule_desktop_shell/src/render/launchpad/paint.rs b/userland/capsule_desktop_shell/src/render/launchpad/paint.rs new file mode 100644 index 0000000000..f023e75df3 --- /dev/null +++ b/userland/capsule_desktop_shell/src/render/launchpad/paint.rs @@ -0,0 +1,26 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Paint the Launchpad: a dark full-screen wash, a title, and every app tile. + +use super::grid::count; +use super::tile; +use crate::render::fill::fill_rect; +use crate::render::text::draw_overlay_text; +use crate::state::Context; + +const OVERLAY_BG: u32 = 0xF2_0A0E_15; +const TITLE_FG: u32 = 0xFFF2_F6FC; +const GLYPH_ADV: u32 = 8; +const TITLE: &[u8] = b"Applications"; + +pub fn paint_launchpad(ctx: &Context) { + fill_rect(ctx.backing_va, ctx.stride, ctx.width, ctx.height, 0, 0, ctx.width, ctx.height, OVERLAY_BG); + let title_w = TITLE.len() as u32 * GLYPH_ADV; + let title_x = ctx.width.saturating_sub(title_w) / 2; + draw_overlay_text(ctx, title_x, 56, TITLE, TITLE_FG); + for i in 0..count() { + tile::paint(ctx, i); + } +} diff --git a/userland/capsule_desktop_shell/src/render/launchpad/tile.rs b/userland/capsule_desktop_shell/src/render/launchpad/tile.rs new file mode 100644 index 0000000000..254a282679 --- /dev/null +++ b/userland/capsule_desktop_shell/src/render/launchpad/tile.rs @@ -0,0 +1,41 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Draw one Launchpad tile: a desktop app uses its own artwork, an installed +//! tool uses its shipped glyph (or a generated tile when it has none), and +//! both carry a centred label underneath. + +use super::grid::{cell_origin, CELL_W, TILE}; +use super::tool_icons; +use crate::render::draw_app_icon; +use crate::render::text::draw_overlay_text; +use crate::state::{Context, LAUNCHER_APPS, TOOL_APPS}; + +const LABEL_FG: u32 = 0xFFE4_EEF8; +const GLYPH_ADV: u32 = 8; +const MAX_LABEL: usize = 14; + +pub(super) fn paint(ctx: &Context, index: usize) { + let (cx, cy) = cell_origin(ctx.width, index); + let icon_x = cx + (CELL_W - TILE) / 2; + let apps = LAUNCHER_APPS.len(); + let label: &[u8] = if index < apps { + let app = &LAUNCHER_APPS[index]; + draw_app_icon(ctx, icon_x, cy, app.icon, TILE); + app.label + } else { + let tool = &TOOL_APPS[index - apps]; + tool_icons::draw(ctx, icon_x, cy, TILE, tool.label); + tool.label + }; + paint_label(ctx, label, cx, cy + TILE + 8); +} + +// The tile label, centred within the cell. +fn paint_label(ctx: &Context, name: &[u8], cx: u32, y: u32) { + let shown = if name.len() > MAX_LABEL { &name[..MAX_LABEL] } else { name }; + let text_w = shown.len() as u32 * GLYPH_ADV; + let x = cx + CELL_W.saturating_sub(text_w) / 2; + draw_overlay_text(ctx, x, y, shown, LABEL_FG); +} diff --git a/userland/capsule_desktop_shell/src/render/launchpad/tool_icons.rs b/userland/capsule_desktop_shell/src/render/launchpad/tool_icons.rs new file mode 100644 index 0000000000..b5eabb652a --- /dev/null +++ b/userland/capsule_desktop_shell/src/render/launchpad/tool_icons.rs @@ -0,0 +1,42 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Real artwork for installed tools. Each shipped tool has a 48x48 glyph mask +//! under assets/app_icons/tools, drawn in the same cyan line-art language as +//! the desktop apps. A tool installed without artwork falls back to the +//! generated tile, so a missing icon never blocks an install. + +use super::gen_icon; +use crate::render::draw_tool_icon; +use crate::state::Context; + +const GREX: &[u8] = include_bytes!("../../../assets/app_icons/tools/grex.rgba"); +const DOTENV_LINTER: &[u8] = include_bytes!("../../../assets/app_icons/tools/dotenv_linter.rgba"); +const PASTEL: &[u8] = include_bytes!("../../../assets/app_icons/tools/pastel.rgba"); +const JSONXF: &[u8] = include_bytes!("../../../assets/app_icons/tools/jsonxf.rgba"); +const CHOOSE: &[u8] = include_bytes!("../../../assets/app_icons/tools/choose.rgba"); +const TOKEI: &[u8] = include_bytes!("../../../assets/app_icons/tools/tokei.rgba"); +const HUNIQ: &[u8] = include_bytes!("../../../assets/app_icons/tools/huniq.rgba"); +const CSVIEW: &[u8] = include_bytes!("../../../assets/app_icons/tools/csview.rgba"); + +fn glyph_for(label: &[u8]) -> Option<&'static [u8]> { + match label { + b"grex" => Some(GREX), + b"dotenv-linter" => Some(DOTENV_LINTER), + b"pastel" => Some(PASTEL), + b"jsonxf" => Some(JSONXF), + b"choose" => Some(CHOOSE), + b"tokei" => Some(TOKEI), + b"huniq" => Some(HUNIQ), + b"csview" => Some(CSVIEW), + _ => None, + } +} + +pub(super) fn draw(ctx: &Context, x: u32, y: u32, size: u32, label: &[u8]) { + match glyph_for(label) { + Some(glyph) => draw_tool_icon(ctx, x, y, size, glyph), + None => gen_icon::draw(ctx, x, y, size, label), + } +} diff --git a/userland/capsule_desktop_shell/src/render/layout.rs b/userland/capsule_desktop_shell/src/render/layout.rs index d441777899..110ea7bb05 100644 --- a/userland/capsule_desktop_shell/src/render/layout.rs +++ b/userland/capsule_desktop_shell/src/render/layout.rs @@ -18,11 +18,18 @@ use crate::state::spotlight::{SPOTLIGHT_HEIGHT, SPOTLIGHT_WIDTH}; use crate::state::LAUNCHER_APPS; pub const MENUBAR_HEIGHT: u32 = 28; -pub const BOTTOM_DOCK_WIDTH: u32 = LAUNCHER_APPS.len() as u32 * (TASKBAR_ENTRY_W + 6) - 6 + 24; +// One slot per desktop app, plus a trailing slot for the Launchpad button. +pub const BOTTOM_DOCK_WIDTH: u32 = + (LAUNCHER_APPS.len() as u32 + 1) * (TASKBAR_ENTRY_W + 6) - 6 + 24; pub const BOTTOM_DOCK_HEIGHT: u32 = 64; pub const BOTTOM_DOCK_BOTTOM_INSET: u32 = 24; pub const TASKBAR_ENTRY_W: u32 = 80; +/// Left edge of the Launchpad button: the slot just past the last app. +pub fn launchpad_slot_x(dock: Rect) -> u32 { + dock.x + 12 + LAUNCHER_APPS.len() as u32 * (TASKBAR_ENTRY_W + 6) +} + #[derive(Clone, Copy, Default)] pub struct Rect { pub x: u32, diff --git a/userland/capsule_desktop_shell/src/render/mod.rs b/userland/capsule_desktop_shell/src/render/mod.rs index 30288ef3e0..b5f2d68587 100644 --- a/userland/capsule_desktop_shell/src/render/mod.rs +++ b/userland/capsule_desktop_shell/src/render/mod.rs @@ -20,6 +20,7 @@ pub mod desktop_icons; pub mod desktop_menu; pub mod fill; mod icons; +pub mod launchpad; pub mod layout; pub mod text; pub mod toasts; @@ -27,6 +28,6 @@ pub mod topbar; pub use bottom_taskbar::paint_bottom_taskbar; pub use chrome::paint_chrome; -pub use icons::draw_app_icon; +pub use icons::{draw_app_icon, draw_tool_icon}; pub use layout::{menubar_rect, spotlight_rect}; pub use toasts::sync_toast_layer; diff --git a/userland/capsule_desktop_shell/src/server/handlers/launcher_focus.rs b/userland/capsule_desktop_shell/src/server/handlers/launcher_focus.rs index ade83e40d4..6c2f8bf450 100644 --- a/userland/capsule_desktop_shell/src/server/handlers/launcher_focus.rs +++ b/userland/capsule_desktop_shell/src/server/handlers/launcher_focus.rs @@ -16,8 +16,9 @@ use nonos_libc::mk_time_millis; -use crate::render::layout::{bottom_dock_rect, TASKBAR_ENTRY_W}; +use crate::render::layout::{bottom_dock_rect, launchpad_slot_x, TASKBAR_ENTRY_W}; use crate::server::handlers::launcher_request::{self, LaunchOutcome}; +use crate::server::handlers::launchpad; use crate::server::refresh_taskbar::refresh_taskbar; use crate::state::{mark_taskbar_launch, Context, NotifyLevel, LAUNCHER_APPS}; @@ -52,4 +53,13 @@ pub fn handle(ctx: &mut Context, x: u32, y: u32) { } row_x += TASKBAR_ENTRY_W + 6; } + // The trailing dock slot is the Launchpad button. + let lp = launchpad_slot_x(bottom); + if x >= lp + && x < lp + TASKBAR_ENTRY_W + && y >= bottom.y + 10 + && y < bottom.y + bottom.height - 10 + { + launchpad::open(ctx); + } } diff --git a/userland/capsule_desktop_shell/src/server/handlers/launcher_request.rs b/userland/capsule_desktop_shell/src/server/handlers/launcher_request.rs index dec0adb4de..601b7fd5d0 100644 --- a/userland/capsule_desktop_shell/src/server/handlers/launcher_request.rs +++ b/userland/capsule_desktop_shell/src/server/handlers/launcher_request.rs @@ -46,10 +46,16 @@ const SINGLE_INSTANCE: [&[u8]; 1] = [b"app.audio_player"]; // an error and we focus the running instance instead, so a click is never a // dead end. Single-instance services skip the spawn and always focus. pub fn request(app: &LauncherApp) -> LaunchOutcome { - if !is_single_instance(app.service) && mk_spawn_instance(app.service) >= 0 { + request_service(app.service) +} + +/// Launch, or focus if already running, whatever capsule owns `service`. Used +/// by both the dock (a desktop app) and the Launchpad (an installed tool). +pub fn request_service(service: &[u8]) -> LaunchOutcome { + if !is_single_instance(service) && mk_spawn_instance(service) >= 0 { return LaunchOutcome::Queued; } - let Some(pid) = lookup_pid(app.service) else { return LaunchOutcome::Failed }; + let Some(pid) = lookup_pid(service) else { return LaunchOutcome::Failed }; let frame = focus_frame(); if mk_ipc_send_to_pid(pid, frame.as_ptr(), frame.len()) >= 0 { LaunchOutcome::Focused diff --git a/userland/capsule_desktop_shell/src/server/handlers/launchpad.rs b/userland/capsule_desktop_shell/src/server/handlers/launchpad.rs new file mode 100644 index 0000000000..b147195e97 --- /dev/null +++ b/userland/capsule_desktop_shell/src/server/handlers/launchpad.rs @@ -0,0 +1,37 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Opening the Launchpad from the dock, and acting on a click while it is open: +//! a tile launches its app or tool, and any click then dismisses the overlay. + +use crate::render::launchpad::{hit, target, Target}; +use crate::server::handlers::launcher_request; +use crate::server::repaint::repaint; +use crate::state::{Context, LAUNCHER_APPS}; + +pub fn open(ctx: &mut Context) { + ctx.launchpad = true; + repaint(ctx); +} + +pub fn click(ctx: &mut Context, px: u32, py: u32) { + if let Some(index) = hit(ctx.width, px, py) { + match target(index) { + Target::App(a) => { + let _ = launcher_request::request(&LAUNCHER_APPS[a]); + } + Target::Tool(_) => { + // A tool is a command-line program: it runs in the terminal, + // where the kernel spawns it parented to the shell so its output + // streams into the scrollback. Opening the terminal is the + // launch; the user runs the tool there by name. + let _ = launcher_request::request_service(b"app.terminal"); + } + } + } + // A click anywhere closes the Launchpad, whether it landed on a tile or on + // empty space, matching how a full-screen launcher is expected to behave. + ctx.launchpad = false; + repaint(ctx); +} diff --git a/userland/capsule_desktop_shell/src/server/handlers/mod.rs b/userland/capsule_desktop_shell/src/server/handlers/mod.rs index 1d90ca883b..d87e684436 100644 --- a/userland/capsule_desktop_shell/src/server/handlers/mod.rs +++ b/userland/capsule_desktop_shell/src/server/handlers/mod.rs @@ -17,6 +17,7 @@ pub mod health; pub mod launcher_focus; pub mod launcher_request; +pub mod launchpad; pub mod notify; pub mod open_with; pub mod spotlight_open; diff --git a/userland/capsule_desktop_shell/src/server/input.rs b/userland/capsule_desktop_shell/src/server/input.rs index dc0d05b42f..2977103d9e 100644 --- a/userland/capsule_desktop_shell/src/server/input.rs +++ b/userland/capsule_desktop_shell/src/server/input.rs @@ -18,7 +18,7 @@ use crate::protocol::{read_i32, read_u16, read_u32}; use crate::render::layout::{bottom_dock_rect, MENUBAR_HEIGHT}; use crate::render::{desktop_icons, desktop_menu, topbar}; use crate::server::desktop; -use crate::server::handlers::{launcher_focus, launcher_request}; +use crate::server::handlers::{launcher_focus, launcher_request, launchpad}; use crate::server::refresh_taskbar::refresh_taskbar; use crate::state::{collapse_taskbar, reveal_taskbar, Context, LAUNCHER_APPS}; use nonos_libc::{ @@ -105,6 +105,12 @@ pub fn handle(ctx: &mut Context, buf: &[u8]) -> bool { return true; } let (px, py) = (x as u32, y as u32); + // The Launchpad, while open, captures every click: a tile launches its app + // or tool, and anything else dismisses the overlay. + if ctx.launchpad { + launchpad::click(ctx, px, py); + return true; + } // While the right-click menu is open, the next click either picks an item // or dismisses it. Handle that before anything else consumes the click. if ctx.desktop_menu.is_some() { diff --git a/userland/capsule_desktop_shell/src/setup/prime/run/build_context.rs b/userland/capsule_desktop_shell/src/setup/prime/run/build_context.rs index 0c935f96d6..d907a719c8 100644 --- a/userland/capsule_desktop_shell/src/setup/prime/run/build_context.rs +++ b/userland/capsule_desktop_shell/src/setup/prime/run/build_context.rs @@ -32,6 +32,7 @@ pub fn build_context(peers: &Peers, overlay: &Overlay) -> Context { tray: TrayTable::new(), taskbar: new_taskbar_state(), spotlight: SpotlightState::new(), + launchpad: false, last_notify_level: None, toasts: ToastQueue::new(), toast_layer_live: false, diff --git a/userland/capsule_desktop_shell/src/state/context.rs b/userland/capsule_desktop_shell/src/state/context.rs index 2645671508..65c38da17a 100644 --- a/userland/capsule_desktop_shell/src/state/context.rs +++ b/userland/capsule_desktop_shell/src/state/context.rs @@ -30,6 +30,8 @@ pub struct Context { pub tray: TrayTable, pub taskbar: TaskbarState, pub spotlight: SpotlightState, + /// Whether the full-screen Launchpad overlay is open. + pub launchpad: bool, pub last_notify_level: Option, pub toasts: ToastQueue, pub toast_layer_live: bool, diff --git a/userland/capsule_desktop_shell/src/state/mod.rs b/userland/capsule_desktop_shell/src/state/mod.rs index 71c2bf6bb1..38bea0e4b1 100644 --- a/userland/capsule_desktop_shell/src/state/mod.rs +++ b/userland/capsule_desktop_shell/src/state/mod.rs @@ -22,9 +22,11 @@ pub mod notify; pub mod spotlight; pub mod taskbar; pub mod toasts; +pub mod tool_apps; pub mod tray; pub use apps::LAUNCHER_APPS; +pub use tool_apps::TOOL_APPS; pub use chrome::TASKBAR_WINDOW_ID; pub use context::Context; pub use notify::NotifyLevel; diff --git a/userland/capsule_desktop_shell/src/state/tool_apps.rs b/userland/capsule_desktop_shell/src/state/tool_apps.rs new file mode 100644 index 0000000000..3b4862220d --- /dev/null +++ b/userland/capsule_desktop_shell/src/state/tool_apps.rs @@ -0,0 +1,25 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! The installed command-line tools shown in the Launchpad, generated by +//! `tools/nonos-app` from `userland/apps.list`. Each tile launches the tool +//! capsule by its service. Do not edit by hand: run `nonos-app add `. + +/// One installed tool: the name on its tile and the service it launches under. +pub struct ToolApp { + pub label: &'static [u8], + pub service: &'static [u8], +} + +/// Every installed tool, generated from `userland/apps.list`. +pub const TOOL_APPS: [ToolApp; 8] = [ + ToolApp { label: b"grex", service: b"tool.grex" }, + ToolApp { label: b"dotenv-linter", service: b"tool.dotenv-linter" }, + ToolApp { label: b"pastel", service: b"tool.pastel" }, + ToolApp { label: b"jsonxf", service: b"tool.jsonxf" }, + ToolApp { label: b"choose", service: b"tool.choose" }, + ToolApp { label: b"tokei", service: b"tool.tokei" }, + ToolApp { label: b"huniq", service: b"tool.huniq" }, + ToolApp { label: b"csview", service: b"tool.csview" }, +]; diff --git a/userland/capsule_dotenv-linter/Capsule.mk b/userland/capsule_dotenv-linter/Capsule.mk new file mode 100644 index 0000000000..f07a040faf --- /dev/null +++ b/userland/capsule_dotenv-linter/Capsule.mk @@ -0,0 +1,21 @@ +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# dotenv-linter: unmodified crates.io source cross-compiled for x86_64-nonos and +# installed as a signed, STARK-attested capsule. Generated by tools/nonos-app. + +CAPSULE_SLUG := dotenv-linter +CAPSULE_HANDLE := dotenv-linter +CAPSULE_DIR := userland/capsule_dotenv-linter +CAPSULE_BIN_NAME := dotenv-linter +CAPSULE_DOMAIN := crates.io +CAPSULE_NAMESPACE := systems.nonos.tool.dotenv-linter +CAPSULE_SERVICE_ENDPOINT := service:4902:tool.dotenv-linter +CAPSULE_REPLY_ENDPOINT := reply:4903:endpoint.tool.dotenv-linter.reply +CAPSULE_REQUIRED_CAPS := 0x19 +CAPSULE_CAPS_CEILING := 0x19 +CAPSULE_PREBUILT_BIN := target/upstream-dotenv-linter/bin/dotenv-linter +CAPSULE_METADATA := crates.io dotenv-linter publisher + +include nonos-mk/capsule.mk diff --git a/userland/capsule_grex/Capsule.mk b/userland/capsule_grex/Capsule.mk new file mode 100644 index 0000000000..9c43c90443 --- /dev/null +++ b/userland/capsule_grex/Capsule.mk @@ -0,0 +1,21 @@ +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# grex: unmodified crates.io source cross-compiled for x86_64-nonos and +# installed as a signed, STARK-attested capsule. Generated by tools/nonos-app. + +CAPSULE_SLUG := grex +CAPSULE_HANDLE := grex +CAPSULE_DIR := userland/capsule_grex +CAPSULE_BIN_NAME := grex +CAPSULE_DOMAIN := crates.io +CAPSULE_NAMESPACE := systems.nonos.tool.grex +CAPSULE_SERVICE_ENDPOINT := service:4900:tool.grex +CAPSULE_REPLY_ENDPOINT := reply:4901:endpoint.tool.grex.reply +CAPSULE_REQUIRED_CAPS := 0x19 +CAPSULE_CAPS_CEILING := 0x19 +CAPSULE_PREBUILT_BIN := target/upstream-grex/bin/grex +CAPSULE_METADATA := crates.io grex publisher + +include nonos-mk/capsule.mk diff --git a/userland/capsule_huniq/Capsule.mk b/userland/capsule_huniq/Capsule.mk new file mode 100644 index 0000000000..456b94bd89 --- /dev/null +++ b/userland/capsule_huniq/Capsule.mk @@ -0,0 +1,21 @@ +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# huniq: unmodified crates.io source cross-compiled for x86_64-nonos and +# installed as a signed, STARK-attested capsule. Generated by tools/nonos-app. + +CAPSULE_SLUG := huniq +CAPSULE_HANDLE := huniq +CAPSULE_DIR := userland/capsule_huniq +CAPSULE_BIN_NAME := huniq +CAPSULE_DOMAIN := crates.io +CAPSULE_NAMESPACE := systems.nonos.tool.huniq +CAPSULE_SERVICE_ENDPOINT := service:4912:tool.huniq +CAPSULE_REPLY_ENDPOINT := reply:4913:endpoint.tool.huniq.reply +CAPSULE_REQUIRED_CAPS := 0x19 +CAPSULE_CAPS_CEILING := 0x19 +CAPSULE_PREBUILT_BIN := target/upstream-huniq/bin/huniq +CAPSULE_METADATA := crates.io huniq publisher + +include nonos-mk/capsule.mk diff --git a/userland/capsule_jsonxf/Capsule.mk b/userland/capsule_jsonxf/Capsule.mk new file mode 100644 index 0000000000..8eefb6c7d3 --- /dev/null +++ b/userland/capsule_jsonxf/Capsule.mk @@ -0,0 +1,21 @@ +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# jsonxf: unmodified crates.io source cross-compiled for x86_64-nonos and +# installed as a signed, STARK-attested capsule. Generated by tools/nonos-app. + +CAPSULE_SLUG := jsonxf +CAPSULE_HANDLE := jsonxf +CAPSULE_DIR := userland/capsule_jsonxf +CAPSULE_BIN_NAME := jsonxf +CAPSULE_DOMAIN := crates.io +CAPSULE_NAMESPACE := systems.nonos.tool.jsonxf +CAPSULE_SERVICE_ENDPOINT := service:4906:tool.jsonxf +CAPSULE_REPLY_ENDPOINT := reply:4907:endpoint.tool.jsonxf.reply +CAPSULE_REQUIRED_CAPS := 0x19 +CAPSULE_CAPS_CEILING := 0x19 +CAPSULE_PREBUILT_BIN := target/upstream-jsonxf/bin/jsonxf +CAPSULE_METADATA := crates.io jsonxf publisher + +include nonos-mk/capsule.mk diff --git a/userland/capsule_pastel/Capsule.mk b/userland/capsule_pastel/Capsule.mk new file mode 100644 index 0000000000..ec8801769a --- /dev/null +++ b/userland/capsule_pastel/Capsule.mk @@ -0,0 +1,21 @@ +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# pastel: unmodified crates.io source cross-compiled for x86_64-nonos and +# installed as a signed, STARK-attested capsule. Generated by tools/nonos-app. + +CAPSULE_SLUG := pastel +CAPSULE_HANDLE := pastel +CAPSULE_DIR := userland/capsule_pastel +CAPSULE_BIN_NAME := pastel +CAPSULE_DOMAIN := crates.io +CAPSULE_NAMESPACE := systems.nonos.tool.pastel +CAPSULE_SERVICE_ENDPOINT := service:4904:tool.pastel +CAPSULE_REPLY_ENDPOINT := reply:4905:endpoint.tool.pastel.reply +CAPSULE_REQUIRED_CAPS := 0x19 +CAPSULE_CAPS_CEILING := 0x19 +CAPSULE_PREBUILT_BIN := target/upstream-pastel/bin/pastel +CAPSULE_METADATA := crates.io pastel publisher + +include nonos-mk/capsule.mk diff --git a/userland/capsule_terminal/src/command/builtin/mod.rs b/userland/capsule_terminal/src/command/builtin/mod.rs index e342d60c33..c60a53cbc2 100644 --- a/userland/capsule_terminal/src/command/builtin/mod.rs +++ b/userland/capsule_terminal/src/command/builtin/mod.rs @@ -30,5 +30,6 @@ pub mod nox; pub mod ping; pub mod service; pub mod theme; +pub mod tool; pub mod version; pub mod whoami; diff --git a/userland/capsule_terminal/src/command/builtin/tool.rs b/userland/capsule_terminal/src/command/builtin/tool.rs new file mode 100644 index 0000000000..fa0da39a21 --- /dev/null +++ b/userland/capsule_terminal/src/command/builtin/tool.rs @@ -0,0 +1,62 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Run one of the baked, attested command-line tools (grex, tokei, csview, ...) +//! from the shell. The kernel spawns the tool parented to this terminal, so the +//! same async drain job that streams a store install's output streams the +//! tool's stdout here. Adding a tool is one line in `TOOLS`. + +use alloc::vec::Vec; +use nonos_libc::mk_tool_run; + +use crate::command::builtin::nox::install::InstallJob; +use crate::term::state::State; + +/// The installed command-line tools, by the bare name typed at the prompt. Each +/// maps to its baked `tool.` service. Kept in step with userland/apps.list. +pub const TOOLS: &[&[u8]] = &[ + b"grex", + b"dotenv-linter", + b"pastel", + b"jsonxf", + b"tokei", + b"huniq", + b"csview", +]; + +pub fn is_tool(name: &[u8]) -> bool { + TOOLS.contains(&name) +} + +/// Spawn the baked tool named by `args[0]` with the rest as its argv, and return +/// a drain job that streams its stdout to the terminal. `None` on a spawn error, +/// with the reason already pushed to the scrollback. +pub fn prepare(state: &mut State, args: &[&[u8]]) -> Option { + let name = args[0]; + let mut service = Vec::with_capacity(5 + name.len()); + service.extend_from_slice(b"tool."); + service.extend_from_slice(name); + + let argv = argv_blob(args); + let rc = mk_tool_run(&service, &argv); + if rc < 0 { + state.scrollback.push_error(b"tool: launch failed"); + state.last_status = 1; + return None; + } + Some(InstallJob::new(rc as u32)) +} + +// argv as the tool sees it: argv[0] is the command name, then each argument, +// NUL-separated, which is what the kernel splits into the process argv. +fn argv_blob(args: &[&[u8]]) -> Vec { + let mut blob = Vec::new(); + for (i, a) in args.iter().enumerate() { + if i > 0 { + blob.push(0); + } + blob.extend_from_slice(a); + } + blob +} diff --git a/userland/capsule_terminal/src/jobs/classify.rs b/userland/capsule_terminal/src/jobs/classify.rs index 130b53bd2b..89f9b82d6c 100644 --- a/userland/capsule_terminal/src/jobs/classify.rs +++ b/userland/capsule_terminal/src/jobs/classify.rs @@ -18,6 +18,7 @@ use alloc::vec::Vec; use crate::command::builtin::nox::install; use crate::command::builtin::ping; +use crate::command::builtin::tool; use crate::command::dispatch::split_stages; use crate::command::output::Output; use crate::term::state::State; @@ -68,24 +69,31 @@ pub fn is_job_command(state: &mut State, args: &[&[u8]]) -> Verdict { Verdict::Handled } }, - // Bare-name run of a bundled store tool (`rg foo`, `sd a b`, ...): route + // Bare-name run of a store tool staged in the vfs (`sd a b`, ...): route // it through the same async installer path as `install `, so the // tool loads and streams its output without blocking the event loop. // The tool name is args[0], which prepare reads as the capsule stem. - name if TOOLS.contains(&name) => match install::prepare(state, args) { + name if STORE_TOOLS.contains(&name) => match install::prepare(state, args) { Some(job) => Verdict::Job(JobWork::InstallDrain(job)), None => { state.last_status = 1; Verdict::Handled } }, + // Bare-name run of a baked, attested tool (`tokei`, `grex foo`, ...): the + // kernel spawns it parented to this terminal and it streams its stdout + // through the same drain job. + name if tool::is_tool(name) => match tool::prepare(state, args) { + Some(job) => Verdict::Job(JobWork::InstallDrain(job)), + None => Verdict::Handled, + }, _ => Verdict::Instant, } } // The CLI tools staged in the vfs store. A bare invocation of one of these runs // it from the store instead of falling through to "unknown verb". -const TOOLS: &[&[u8]] = &[b"sd", b"tokio-smoke", b"std_proof"]; +const STORE_TOOLS: &[&[u8]] = &[b"sd", b"tokio-smoke", b"std_proof"]; fn is_plain(args: &[&[u8]]) -> bool { !args.iter().any(|a| matches!(*a, b"|" | b">" | b">>" | b"<")) diff --git a/userland/capsule_tokei/Capsule.mk b/userland/capsule_tokei/Capsule.mk new file mode 100644 index 0000000000..0a2f9bfff1 --- /dev/null +++ b/userland/capsule_tokei/Capsule.mk @@ -0,0 +1,21 @@ +# NONOS Operating System +# Copyright (C) 2026 NONOS Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# tokei: unmodified crates.io source cross-compiled for x86_64-nonos and +# installed as a signed, STARK-attested capsule. Generated by tools/nonos-app. + +CAPSULE_SLUG := tokei +CAPSULE_HANDLE := tokei +CAPSULE_DIR := userland/capsule_tokei +CAPSULE_BIN_NAME := tokei +CAPSULE_DOMAIN := crates.io +CAPSULE_NAMESPACE := systems.nonos.tool.tokei +CAPSULE_SERVICE_ENDPOINT := service:4910:tool.tokei +CAPSULE_REPLY_ENDPOINT := reply:4911:endpoint.tool.tokei.reply +CAPSULE_REQUIRED_CAPS := 0x19 +CAPSULE_CAPS_CEILING := 0x19 +CAPSULE_PREBUILT_BIN := target/upstream-tokei/bin/tokei +CAPSULE_METADATA := crates.io tokei publisher + +include nonos-mk/capsule.mk diff --git a/userland/libc/src/lib.rs b/userland/libc/src/lib.rs index 89ef676d4e..8fe7f6468a 100644 --- a/userland/libc/src/lib.rs +++ b/userland/libc/src/lib.rs @@ -34,6 +34,7 @@ pub mod proc_output; pub mod process; pub mod procstat; pub mod spawn_instance; +pub mod tool_run; pub mod surface_registry; mod syscall; pub mod time; @@ -54,6 +55,7 @@ pub use broker::{ }; pub use capsule_load::{mk_capsule_load, CapsuleLoadRequest}; pub use spawn_instance::mk_spawn_instance; +pub use tool_run::mk_tool_run; pub use crypto::{ crypto_decrypt, crypto_decrypt_aad, crypto_ed25519_verify, crypto_encrypt, crypto_encrypt_aad, crypto_hash, crypto_hkdf_sha256, crypto_hmac_sha256, crypto_keccak256, diff --git a/userland/libc/src/syscall/mod.rs b/userland/libc/src/syscall/mod.rs index 5835c3a596..0fe643e3e6 100644 --- a/userland/libc/src/syscall/mod.rs +++ b/userland/libc/src/syscall/mod.rs @@ -37,6 +37,7 @@ pub(crate) use numbers::{ N_MK_PCI_CONFIG_WRITE, N_MK_PID_ALIVE, N_MK_PIO_GRANT, N_MK_PIO_READ, N_MK_PIO_RELEASE, N_MK_PIO_WRITE, N_MK_PROC_INPUT, N_MK_PROC_OUTPUT, N_MK_PROC_STAT, N_MK_SERVICE_LOOKUP, N_MK_SERVICE_REGISTER, N_MK_SPAWN_INSTANCE, N_MK_STDIN_READ, N_MK_SURFACE_ATTACH, + N_MK_TOOL_RUN, N_MK_SURFACE_PRESENT, N_MK_SURFACE_REGISTER, N_MK_SURFACE_RELEASE, N_MK_SURFACE_SHARE, N_MK_TIME_ADJUST, N_MK_TIME_MILLIS, N_MK_TIME_RTC, N_MK_UPTIME_MS, N_MK_WAIT, N_MK_YIELD, }; diff --git a/userland/libc/src/syscall/numbers/core.rs b/userland/libc/src/syscall/numbers/core.rs index 50b4336d92..bdd15b055e 100644 --- a/userland/libc/src/syscall/numbers/core.rs +++ b/userland/libc/src/syscall/numbers/core.rs @@ -31,6 +31,7 @@ pub(crate) const N_MK_PROC_STAT: i64 = tag4(b"MPST"); pub(crate) const N_MK_PROC_OUTPUT: i64 = tag4(b"MOUT"); pub(crate) const N_MK_ATTEST_STATUS: i64 = tag4(b"MAST"); pub(crate) const N_MK_SPAWN_INSTANCE: i64 = tag4(b"MSPI"); +pub(crate) const N_MK_TOOL_RUN: i64 = tag4(b"MTRN"); pub(crate) const N_MK_WAIT: i64 = tag4(b"MWAT"); pub(crate) const N_MK_KILL: i64 = tag4(b"MKIL"); pub(crate) const N_MK_PROC_INPUT: i64 = tag4(b"MPIN"); diff --git a/userland/libc/src/tool_run.rs b/userland/libc/src/tool_run.rs new file mode 100644 index 0000000000..e77b64e1b9 --- /dev/null +++ b/userland/libc/src/tool_run.rs @@ -0,0 +1,26 @@ +// NONOS Operating System +// Copyright (C) 2026 NONOS Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +//! Run a baked, attested command-line tool by name. The tool is spawned +//! parented to the caller, so the caller drains its stdout with `mk_proc_output` +//! and feeds its stdin with `mk_proc_input`, and waits for exit with `mk_wait`. + +use crate::syscall::{call_raw, N_MK_TOOL_RUN}; + +/// Run the baked tool named `name` (for example `b"tool.tokei"`) with `argv`, a +/// NUL-separated `name\0arg1\0...` blob (empty for none). Returns the new tool's +/// pid, or a negative errno. +pub fn mk_tool_run(name: &[u8], argv: &[u8]) -> i64 { + call_raw( + N_MK_TOOL_RUN, + [ + name.as_ptr() as u64, + name.len() as u64, + argv.as_ptr() as u64, + argv.len() as u64, + 0, + 0, + ], + ) +} diff --git a/userland/upstream-src/atty-0.2.14/.cargo-ok b/userland/upstream-src/atty-0.2.14/.cargo-ok new file mode 100644 index 0000000000..5f8b795830 --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/userland/upstream-src/atty-0.2.14/.cargo_vcs_info.json b/userland/upstream-src/atty-0.2.14/.cargo_vcs_info.json new file mode 100644 index 0000000000..b4096169ee --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/.cargo_vcs_info.json @@ -0,0 +1,5 @@ +{ + "git": { + "sha1": "7b5df17888997d57c2c1c8f91da1db5691f49953" + } +} diff --git a/userland/upstream-src/atty-0.2.14/.gitignore b/userland/upstream-src/atty-0.2.14/.gitignore new file mode 100644 index 0000000000..afc9901273 --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +*.bk diff --git a/userland/upstream-src/atty-0.2.14/CHANGELOG.md b/userland/upstream-src/atty-0.2.14/CHANGELOG.md new file mode 100644 index 0000000000..4e9673f0de --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/CHANGELOG.md @@ -0,0 +1,73 @@ +# 0.2.14 + +* add support for [RustyHermit](https://github.com/hermitcore/libhermit-rs), a Rust-based unikernel [#41](https://github.com/softprops/atty/pull/41) + +# 0.2.13 + +* support older versions of rust that do now support 2018 edition + +# 0.2.12 + +* Redox is now in the unix family so redox cfg is no longer needed [#35](https://github.com/softprops/atty/pull/35) + +# 0.2.11 + +* fix msys detection with `winapi@0.3.5` [#28](https://github.com/softprops/atty/pull/28) + +# 0.2.10 + +* fix wasm regression [#27](https://github.com/softprops/atty/pull/27) + +# 0.2.9 + +* Fix fix pty detection [#25](https://github.com/softprops/atty/pull/25) + +# 0.2.8 + +* Fix an inverted condition on MinGW [#22](https://github.com/softprops/atty/pull/22) + +# 0.2.7 + +* Change `||` to `&&` for whether MSYS is a tty [#24](https://github.com/softprops/atty/pull/24/) + +# 0.2.6 + +* updated winapi dependency to [0.3](https://retep998.github.io/blog/winapi-0.3/) [#18](https://github.com/softprops/atty/pull/18) + +# 0.2.5 + +* added support for Wasm compile targets [#17](https://github.com/softprops/atty/pull/17) + +# 0.2.4 + +* added support for Wasm compile targets [#17](https://github.com/softprops/atty/pull/17) + +# 0.2.3 + +* added support for Redox OS [#14](https://github.com/softprops/atty/pull/14) + +# 0.2.2 + +* use target specific dependencies [#11](https://github.com/softprops/atty/pull/11) +* Add tty detection for MSYS terminals [#12](https://github.com/softprops/atty/pull/12) + +# 0.2.1 + +* fix windows bug + +# 0.2.0 + +* support for various stream types + +# 0.1.2 + +* windows support (with automated testing) +* automated code coverage + +# 0.1.1 + +* bumped libc dep from `0.1` to `0.2` + +# 0.1.0 + +* initial release diff --git a/userland/upstream-src/atty-0.2.14/Cargo.toml b/userland/upstream-src/atty-0.2.14/Cargo.toml new file mode 100644 index 0000000000..d6bf2d03b3 --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/Cargo.toml @@ -0,0 +1,34 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +name = "atty" +version = "0.2.14" +authors = ["softprops "] +exclude = ["/.travis.yml", "/appveyor.yml"] +description = "A simple interface for querying atty" +homepage = "https://github.com/softprops/atty" +documentation = "http://softprops.github.io/atty" +readme = "README.md" +keywords = ["terminal", "tty", "isatty"] +license = "MIT" +repository = "https://github.com/softprops/atty" +[target."cfg(target_os = \"hermit\")".dependencies.hermit-abi] +version = "0.1.6" +[target."cfg(unix)".dependencies.libc] +version = "0.2" +default-features = false +[target."cfg(windows)".dependencies.winapi] +version = "0.3" +features = ["consoleapi", "processenv", "minwinbase", "minwindef", "winbase"] +[badges.travis-ci] +repository = "softprops/atty" diff --git a/userland/upstream-src/atty-0.2.14/Cargo.toml.orig b/userland/upstream-src/atty-0.2.14/Cargo.toml.orig new file mode 100644 index 0000000000..aa2cbb77e1 --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/Cargo.toml.orig @@ -0,0 +1,26 @@ +[package] +name = "atty" +version = "0.2.14" +authors = ["softprops "] +description = "A simple interface for querying atty" +documentation = "http://softprops.github.io/atty" +homepage = "https://github.com/softprops/atty" +repository = "https://github.com/softprops/atty" +keywords = ["terminal", "tty", "isatty"] +license = "MIT" +readme = "README.md" +exclude = ["/.travis.yml", "/appveyor.yml"] + +[badges] +travis-ci = { repository = "softprops/atty" } + +[target.'cfg(unix)'.dependencies] +libc = { version = "0.2", default-features = false } + +[target.'cfg(target_os = "hermit")'.dependencies] +hermit-abi = "0.1.6" + +[target.'cfg(windows)'.dependencies.winapi] +version = "0.3" +features = ["consoleapi", "processenv", "minwinbase", "minwindef", "winbase"] + diff --git a/userland/upstream-src/atty-0.2.14/LICENSE b/userland/upstream-src/atty-0.2.14/LICENSE new file mode 100644 index 0000000000..b9da76b735 --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2015-2019 Doug Tangren + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/userland/upstream-src/atty-0.2.14/README.md b/userland/upstream-src/atty-0.2.14/README.md new file mode 100644 index 0000000000..cd85593d6e --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/README.md @@ -0,0 +1,74 @@ +# atty + +[![Build Status](https://travis-ci.org/softprops/atty.svg?branch=master)](https://travis-ci.org/softprops/atty) [![Build status](https://ci.appveyor.com/api/projects/status/geggrsnsjsuse8cv?svg=true)](https://ci.appveyor.com/project/softprops/atty) [![Coverage Status](https://coveralls.io/repos/softprops/atty/badge.svg?branch=master&service=github)](https://coveralls.io/github/softprops/atty?branch=master) [![crates.io](https://img.shields.io/crates/v/atty.svg)](https://crates.io/crates/atty) [![Released API docs](https://docs.rs/atty/badge.svg)](http://docs.rs/atty) [![Master API docs](https://img.shields.io/badge/docs-master-green.svg)](https://softprops.github.io/atty) + +> are you or are you not a tty? + + +## install + +Add the following to your `Cargo.toml` + +```toml +[dependencies] +atty = "0.2" +``` + +## usage + +```rust +use atty::Stream; + +fn main() { + if atty::is(Stream::Stdout) { + println!("I'm a terminal"); + } else { + println!("I'm not"); + } +} +``` + +## testing + +This library has been unit tested on both unix and windows platforms (via appveyor). + + +A simple example program is provided in this repo to test various tty's. By default. + +It prints + +```bash +$ cargo run --example atty +stdout? true +stderr? true +stdin? true +``` + +To test std in, pipe some text to the program + +```bash +$ echo "test" | cargo run --example atty +stdout? true +stderr? true +stdin? false +``` + +To test std out, pipe the program to something + +```bash +$ cargo run --example atty | grep std +stdout? false +stderr? true +stdin? true +``` + +To test std err, pipe the program to something redirecting std err + +```bash +$ cargo run --example atty 2>&1 | grep std +stdout? false +stderr? false +stdin? true +``` + +Doug Tangren (softprops) 2015-2019 diff --git a/userland/upstream-src/atty-0.2.14/examples/atty.rs b/userland/upstream-src/atty-0.2.14/examples/atty.rs new file mode 100644 index 0000000000..3b3635e59c --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/examples/atty.rs @@ -0,0 +1,9 @@ +extern crate atty; + +use atty::{is, Stream}; + +fn main() { + println!("stdout? {}", is(Stream::Stdout)); + println!("stderr? {}", is(Stream::Stderr)); + println!("stdin? {}", is(Stream::Stdin)); +} diff --git a/userland/upstream-src/atty-0.2.14/rustfmt.toml b/userland/upstream-src/atty-0.2.14/rustfmt.toml new file mode 100644 index 0000000000..899a094cd5 --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/rustfmt.toml @@ -0,0 +1,4 @@ +# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#fn_args_layout +fn_args_layout = "Vertical" +# https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#merge_imports +merge_imports = true \ No newline at end of file diff --git a/userland/upstream-src/atty-0.2.14/src/lib.rs b/userland/upstream-src/atty-0.2.14/src/lib.rs new file mode 100644 index 0000000000..dd8b652cff --- /dev/null +++ b/userland/upstream-src/atty-0.2.14/src/lib.rs @@ -0,0 +1,218 @@ +//! atty is a simple utility that answers one question +//! > is this a tty? +//! +//! usage is just as simple +//! +//! ``` +//! if atty::is(atty::Stream::Stdout) { +//! println!("i'm a tty") +//! } +//! ``` +//! +//! ``` +//! if atty::isnt(atty::Stream::Stdout) { +//! println!("i'm not a tty") +//! } +//! ``` + +#![cfg_attr(unix, no_std)] + +#[cfg(unix)] +extern crate libc; +#[cfg(windows)] +extern crate winapi; + +#[cfg(windows)] +use winapi::shared::minwindef::DWORD; +#[cfg(windows)] +use winapi::shared::ntdef::WCHAR; + +/// possible stream sources +#[derive(Clone, Copy, Debug)] +pub enum Stream { + Stdout, + Stderr, + Stdin, +} + +/// returns true if this is a tty +#[cfg(all(unix, not(target_arch = "wasm32")))] +pub fn is(stream: Stream) -> bool { + extern crate libc; + + let fd = match stream { + Stream::Stdout => libc::STDOUT_FILENO, + Stream::Stderr => libc::STDERR_FILENO, + Stream::Stdin => libc::STDIN_FILENO, + }; + unsafe { libc::isatty(fd) != 0 } +} + +/// returns true if this is a tty +#[cfg(target_os = "hermit")] +pub fn is(stream: Stream) -> bool { + extern crate hermit_abi; + + let fd = match stream { + Stream::Stdout => hermit_abi::STDOUT_FILENO, + Stream::Stderr => hermit_abi::STDERR_FILENO, + Stream::Stdin => hermit_abi::STDIN_FILENO, + }; + hermit_abi::isatty(fd) +} + +/// returns true if this is a tty +#[cfg(windows)] +pub fn is(stream: Stream) -> bool { + use winapi::um::winbase::{ + STD_ERROR_HANDLE as STD_ERROR, STD_INPUT_HANDLE as STD_INPUT, + STD_OUTPUT_HANDLE as STD_OUTPUT, + }; + + let (fd, others) = match stream { + Stream::Stdin => (STD_INPUT, [STD_ERROR, STD_OUTPUT]), + Stream::Stderr => (STD_ERROR, [STD_INPUT, STD_OUTPUT]), + Stream::Stdout => (STD_OUTPUT, [STD_INPUT, STD_ERROR]), + }; + if unsafe { console_on_any(&[fd]) } { + // False positives aren't possible. If we got a console then + // we definitely have a tty on stdin. + return true; + } + + // At this point, we *could* have a false negative. We can determine that + // this is true negative if we can detect the presence of a console on + // any of the other streams. If another stream has a console, then we know + // we're in a Windows console and can therefore trust the negative. + if unsafe { console_on_any(&others) } { + return false; + } + + // Otherwise, we fall back to a very strange msys hack to see if we can + // sneakily detect the presence of a tty. + unsafe { msys_tty_on(fd) } +} + +/// returns true if this is _not_ a tty +pub fn isnt(stream: Stream) -> bool { + !is(stream) +} + +/// Returns true if any of the given fds are on a console. +#[cfg(windows)] +unsafe fn console_on_any(fds: &[DWORD]) -> bool { + use winapi::um::{consoleapi::GetConsoleMode, processenv::GetStdHandle}; + + for &fd in fds { + let mut out = 0; + let handle = GetStdHandle(fd); + if GetConsoleMode(handle, &mut out) != 0 { + return true; + } + } + false +} + +/// Returns true if there is an MSYS tty on the given handle. +#[cfg(windows)] +unsafe fn msys_tty_on(fd: DWORD) -> bool { + use std::{mem, slice}; + + use winapi::{ + ctypes::c_void, + shared::minwindef::MAX_PATH, + um::{ + fileapi::FILE_NAME_INFO, minwinbase::FileNameInfo, processenv::GetStdHandle, + winbase::GetFileInformationByHandleEx, + }, + }; + + let size = mem::size_of::(); + let mut name_info_bytes = vec![0u8; size + MAX_PATH * mem::size_of::()]; + let res = GetFileInformationByHandleEx( + GetStdHandle(fd), + FileNameInfo, + &mut *name_info_bytes as *mut _ as *mut c_void, + name_info_bytes.len() as u32, + ); + if res == 0 { + return false; + } + let name_info: &FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const FILE_NAME_INFO); + let s = slice::from_raw_parts( + name_info.FileName.as_ptr(), + name_info.FileNameLength as usize / 2, + ); + let name = String::from_utf16_lossy(s); + // This checks whether 'pty' exists in the file name, which indicates that + // a pseudo-terminal is attached. To mitigate against false positives + // (e.g., an actual file name that contains 'pty'), we also require that + // either the strings 'msys-' or 'cygwin-' are in the file name as well.) + let is_msys = name.contains("msys-") || name.contains("cygwin-"); + let is_pty = name.contains("-pty"); + is_msys && is_pty +} + +/// returns true if this is a tty +#[cfg(target_arch = "wasm32")] +pub fn is(_stream: Stream) -> bool { + false +} + +/// returns true if this is a tty. On NONOS a capsule's stdio is wired to the +/// nox terminal, so it is always a tty and tools enable color and interactive +/// output. +#[cfg(target_vendor = "nonos")] +pub fn is(_stream: Stream) -> bool { + true +} + +#[cfg(test)] +mod tests { + use super::{is, Stream}; + + #[test] + #[cfg(windows)] + fn is_err() { + // appveyor pipes its output + assert!(!is(Stream::Stderr)) + } + + #[test] + #[cfg(windows)] + fn is_out() { + // appveyor pipes its output + assert!(!is(Stream::Stdout)) + } + + #[test] + #[cfg(windows)] + fn is_in() { + assert!(is(Stream::Stdin)) + } + + #[test] + #[cfg(unix)] + fn is_err() { + assert!(is(Stream::Stderr)) + } + + #[test] + #[cfg(unix)] + fn is_out() { + assert!(is(Stream::Stdout)) + } + + #[test] + #[cfg(target_os = "macos")] + fn is_in() { + // macos on travis seems to pipe its input + assert!(is(Stream::Stdin)) + } + + #[test] + #[cfg(all(not(target_os = "macos"), unix))] + fn is_in() { + assert!(is(Stream::Stdin)) + } +} diff --git a/userland/upstream-src/choose/.cargo-ok b/userland/upstream-src/choose/.cargo-ok new file mode 100644 index 0000000000..5f8b795830 --- /dev/null +++ b/userland/upstream-src/choose/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/userland/upstream-src/choose/.cargo_vcs_info.json b/userland/upstream-src/choose/.cargo_vcs_info.json new file mode 100644 index 0000000000..192a227c55 --- /dev/null +++ b/userland/upstream-src/choose/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "8d64b136406da28fb9b48db8506578aef1c221b3" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/userland/upstream-src/choose/.github/workflows/release.yml b/userland/upstream-src/choose/.github/workflows/release.yml new file mode 100644 index 0000000000..9c59e0b16b --- /dev/null +++ b/userland/upstream-src/choose/.github/workflows/release.yml @@ -0,0 +1,134 @@ +name: Create Releases + +on: + push: + tags: + - v* + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Check style + run: cargo fmt -- --check + + - name: Run unit tests + run: cargo test --verbose + + - name: Rust end-to-end test + run: test/e2e_test.sh + + - name: Build x86_64-unknown-linux-gnu + run: cargo build --verbose --release --target x86_64-unknown-linux-gnu + + - name: Build x86_64-unknown-linux-musl + run: | + rustup target add x86_64-unknown-linux-musl + cargo build --verbose --release --target x86_64-unknown-linux-musl + + - name: Build aarch64-unknown-linux-gnu + run: | + sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu + rustup target add aarch64-unknown-linux-gnu + cargo build --verbose --release --target aarch64-unknown-linux-gnu --config target.aarch64-unknown-linux-gnu.linker=\"aarch64-linux-gnu-gcc\" + + - name: Build aarch64-apple-darwin + run: | + curl -L https://github.com/roblabla/MacOSX-SDKs/releases/download/13.3/MacOSX13.3.sdk.tar.xz | tar xJ + export SDKROOT=$(pwd)/MacOSX13.3.sdk/ + export PATH=$PATH:~/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/aarch64-apple-darwin/bin/ + export CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=rust-lld + rustup target add aarch64-apple-darwin + cargo build --verbose --release --target aarch64-apple-darwin + rm -rf $SDKROOT + + - name: Build x86_64-pc-windows-gnu + run: | + sudo apt-get update && sudo apt-get install -y mingw-w64 + rustup target add x86_64-pc-windows-gnu + cargo build --verbose --release --target x86_64-pc-windows-gnu + + - name: Create GitHub release + id: create-release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body: '' + draft: false + prerelease: false + + - name: Upload binary to GitHub release + id: upload-x86_64-unknown-linux-gnu-release-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: target/x86_64-unknown-linux-gnu/release/choose + asset_name: choose-x86_64-unknown-linux-gnu + asset_content_type: application/raw + + - name: Upload musl binary to GitHub release + id: upload-x86_64-unknown-linux-musl-release-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: target/x86_64-unknown-linux-musl/release/choose + asset_name: choose-x86_64-unknown-linux-musl + asset_content_type: application/raw + + - name: Upload aarch64 linux binary to GitHub release + id: upload-aarch64-unknown-linux-gnu-release-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: target/aarch64-unknown-linux-gnu/release/choose + asset_name: choose-aarch64-unknown-linux-gnu + asset_content_type: application/raw + + - name: Upload aarch64 darwin binary to GitHub release + id: upload-aarch64-apple-darwin-release-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: target/aarch64-apple-darwin/release/choose + asset_name: choose-aarch64-apple-darwin + asset_content_type: application/raw + + - name: Upload mingw binary to GitHub release + id: upload-x86_64-pc-windows-gnu-release-asset + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: target/x86_64-pc-windows-gnu/release/choose.exe + asset_name: choose-x86_64-pc-windows-gnu + asset_content_type: application/raw + + - name: Upload mingw .exe binary to GitHub release + id: upload-x86_64-pc-windows-gnu-release-asset-exe + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: target/x86_64-pc-windows-gnu/release/choose.exe + asset_name: choose-x86_64-pc-windows-gnu.exe + asset_content_type: application/raw + + - name: Create crates.io release + run: cargo publish --token ${{ secrets.CRATES_IO_TOKEN }} diff --git a/userland/upstream-src/choose/.github/workflows/rust.yml b/userland/upstream-src/choose/.github/workflows/rust.yml new file mode 100644 index 0000000000..af1c9d9a9d --- /dev/null +++ b/userland/upstream-src/choose/.github/workflows/rust.yml @@ -0,0 +1,35 @@ +name: Rust + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Check style + run: cargo fmt -- --check + + # this will report success even if there are warnings, and should be manually checked for now + - name: Lint + run: cargo clippy + + - name: Build + run: cargo build --verbose + + - name: Run unit tests + run: cargo test --verbose + + - name: Run end-to-end tests + run: test/e2e_test.sh + diff --git a/userland/upstream-src/choose/.gitignore b/userland/upstream-src/choose/.gitignore new file mode 100644 index 0000000000..24e2899400 --- /dev/null +++ b/userland/upstream-src/choose/.gitignore @@ -0,0 +1,9 @@ +/target +**/*.rs.bk +tags +todo +bench_output/ +**/*.bench +**/*.svg +test/long*txt +perf.data* diff --git a/userland/upstream-src/choose/Cargo.lock b/userland/upstream-src/choose/Cargo.lock new file mode 100644 index 0000000000..c01b6dd18a --- /dev/null +++ b/userland/upstream-src/choose/Cargo.lock @@ -0,0 +1,278 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "backslash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc9fc0782bff70e4ea109606bc4e37162191a39a513416ae04c8fcadc983e42d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "choose" +version = "1.3.7" +dependencies = [ + "backslash", + "lazy_static", + "regex", + "structopt", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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" diff --git a/userland/upstream-src/choose/Cargo.toml b/userland/upstream-src/choose/Cargo.toml new file mode 100644 index 0000000000..e7fa3d9814 --- /dev/null +++ b/userland/upstream-src/choose/Cargo.toml @@ -0,0 +1,49 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "choose" +version = "1.3.7" +authors = ["Ryan Geary "] +build = false +exclude = ["test/*"] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A human-friendly and fast alternative to cut and (sometimes) awk" +homepage = "https://github.com/theryangeary/choose" +documentation = "https://github.com/theryangeary/choose" +readme = "readme.md" +license = "GPL-3.0-or-later" +repository = "https://github.com/theryangeary/choose" + +[[bin]] +name = "choose" +path = "src/main.rs" + +[dependencies.backslash] +version = "0" + +[dependencies.lazy_static] +version = "1" + +[dependencies.regex] +version = "1" + +[dependencies.structopt] +version = "0.3" + +# nonos-app-patch +[patch.crates-io] +atty = { path = "../atty-0.2.14" } diff --git a/userland/upstream-src/choose/Cargo.toml.orig b/userland/upstream-src/choose/Cargo.toml.orig new file mode 100644 index 0000000000..d8bd015bad --- /dev/null +++ b/userland/upstream-src/choose/Cargo.toml.orig @@ -0,0 +1,22 @@ +[package] +name = "choose" +version = "1.3.7" +authors = ["Ryan Geary "] +edition = "2021" +license = "GPL-3.0-or-later" +description = "A human-friendly and fast alternative to cut and (sometimes) awk" +homepage = "https://github.com/theryangeary/choose" +documentation = "https://github.com/theryangeary/choose" +repository = "https://github.com/theryangeary/choose" +readme = "readme.md" +exclude = [ + "test/*", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +structopt = "0.3" +regex = "1" +lazy_static = "1" +backslash = "0" diff --git a/userland/upstream-src/choose/LICENSE b/userland/upstream-src/choose/LICENSE new file mode 100644 index 0000000000..f288702d2f --- /dev/null +++ b/userland/upstream-src/choose/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/userland/upstream-src/choose/Makefile b/userland/upstream-src/choose/Makefile new file mode 100644 index 0000000000..bed925d4ca --- /dev/null +++ b/userland/upstream-src/choose/Makefile @@ -0,0 +1,27 @@ +.PHONY: release +release: + cargo build --release + +.PHONY: release-debug +release-debug: + RUSTFLAGS=-g cargo build --release + +flamegraph: release-debug + perf record --call-graph dwarf,16384 -e cpu-clock -F 997 target/release/choose -i test/long_long_long_long.txt 3:5 + perf script | stackcollapse-perf.pl | stackcollapse-recursive.pl | c++filt | flamegraph.pl > flamegraphs/working.svg + +flamegraph_commit: release-debug + perf record --call-graph dwarf,16384 -e cpu-clock -F 997 target/release/choose -i test/long_long_long_long.txt 3:5 + perf script | stackcollapse-perf.pl | stackcollapse-recursive.pl | c++filt | flamegraph.pl > flamegraphs/`git log -n 1 --pretty=format:"%h"`.svg + +.PHONY: test +test: + cargo test + test/e2e_test.sh + +bench: release + test/bench.sh working + +bench_commit: release + test/bench.sh `git log -n 1 --pretty=format:"%h"` + diff --git a/userland/upstream-src/choose/contributing.md b/userland/upstream-src/choose/contributing.md new file mode 100644 index 0000000000..0e63d0fcdf --- /dev/null +++ b/userland/upstream-src/choose/contributing.md @@ -0,0 +1,116 @@ +# Contributing + +Thank you for considering contributing to `choose`! + +To save your time and mine, I will attempt to maintain brevity in this +document, adding more details where there are common questions or +misunderstandings. + +## Where To Start + +If you have found a bug or would like to request a feature, [open an +issue](https://github.com/theryangeary/choose/issues/new). + +It is best if you get confirmation of your bug or approval for a feature +request before taking the time to write the code. + +## Fork && Create Branch + +If you have confirmation/approval and would like to try your hand at making the +change, [fork `choose`](https://help.github.com/articles/fork-a-repo) and create +a branch with a descriptive name. + +Branch off of `develop`. Bug fix branches should be named +`hotfix/` and feature branches should be named +`feature/`. **Any hotfix or feature branch should only address +one issue/feature**. + +```sh +git checkout -b develop +``` + +## Check The Test Suite + +Before making any changes, make sure that both the unit tests and the end-to-end +tests all work. + +```sh +cargo test +test/e2e_test.sh +``` + +If you are planning on making changes that may affect performance, consider +using the benchmark script `test/bench.sh` as well. + +## Implement Your Fix/Feature + +### Write tests + +Tests are important. + +If you are fixing a bug, add tests that identify that bug and any permutations +of it you can find, so we can ensure it doesn't come back. + +If you are creating a feature, add tests that will comprehensively ensure the +feature works as expected, in conjunction with all other features, switches, +options, etc. + +### Write code + +It should be correct. It should be fast. It should be idiomatic. Ask for help if +you need it, don't be shy. + +### Write documentation (if needed) + +If your feature adds a new command line switch or option, add that info to the +readme or any other relevant locations. + +## Make a Pull Request + +Once you've finished your changes, make sure that your develop branch is up to +date. + +```sh +git remote add upstream git@github.com:theryangeary/choose.git +git checkout develop +git pull upstream develop +``` + +Check that your code is all formatted correctly. If not, commit any changes. + +```sh +git checkout +cargo fmt +git status +``` + +Rebase and squash your branch on develop. This will prompt you with a list of +your commits. Change all but the first commit to "squash". Write a nice +changelog message in the resulting commit. + +```sh +git rebase -i develop +``` + +Push to your fork. + +```sh +git push --set-upstream origin +``` + +Go to GitHub and [make a Pull +Request](https://help.github.com/articles/creating-a-pull-request)! Make sure +that your Pull Request is against `develop` and not `master`! + +## Keep Your Pull Request Updated + +After making your Pull Request, you may be asked to make some changes. After +completing and commiting your changes, you will need to rebase and resquash your +commits. Each Pull Request will effectively be a single commit added to the +`develop` branch. + +After changing and committing, push like this: + +```sh +git push --force-with-lease +``` diff --git a/userland/upstream-src/choose/readme.md b/userland/upstream-src/choose/readme.md new file mode 100644 index 0000000000..9d4b4503d5 --- /dev/null +++ b/userland/upstream-src/choose/readme.md @@ -0,0 +1,182 @@ +# Choose + +This is `choose`, a human-friendly and fast alternative to `cut` and (sometimes) `awk` + +[![`choose` demo](https://asciinema.org/a/315932.png)](https://asciinema.org/a/315932?autoplay=1) + +## Features + +- terse field selection syntax similar to Python's list slices +- negative indexing from end of line +- optional start/end index +- zero-indexed +- reverse ranges +- slightly faster than `cut` for sufficiently long inputs, much faster than + `awk` +- regular expression field separators using Rust's regex syntax + +## Rationale + +The AWK programming language is designed for text processing and is extremely +capable in this endeavor. However, the `awk` command is not ideal for rapid +shell use, with its requisite quoting of a line wrapped in curly braces, even +for the simplest of programs: + +```bash +awk '{print $1}' +``` + +Likewise, `cut` is far from ideal for rapid shell use, because of its confusing +syntax. Field separators and ranges are just plain difficult to get right on the +first try. + +It is for these reasons that I present to you `choose`. It is not meant to be a +drop-in or complete replacement for either of the aforementioned tools, but +rather a simple and intuitive tool to reach for when the basics of `awk` or +`cut` will do, but the overhead of getting them to behave should not be +necessary. + +## Contributing + +Please see our guidelines in [contributing.md](contributing.md). + +## Usage + +``` +$ choose --help +choose 1.2.0 +`choose` sections from each line of files + +USAGE: + choose [FLAGS] [OPTIONS] ... + +FLAGS: + -c, --character-wise Choose fields by character number + -d, --debug Activate debug mode + -x, --exclusive Use exclusive ranges, similar to array indexing in many programming languages + -h, --help Prints help information + -n, --non-greedy Use non-greedy field separators + -V, --version Prints version information + +OPTIONS: + -f, --field-separator + Specify field separator other than whitespace, using Rust `regex` syntax + + -i, --input Input file + -o, --output-field-separator Specify output field separator + +ARGS: + ... Fields to print. Either a, a:b, a..b, or a..=b, where a and b are integers. The beginning or end + of a range can be omitted, resulting in including the beginning or end of the line, + respectively. a:b is inclusive of b (unless overridden by -x). a..b is exclusive of b and a..=b + is inclusive of b +``` + +### Examples + +```bash +choose 5 # print the 5th item from a line (zero indexed) + +choose -f ':' 0 3 5 # print the 0th, 3rd, and 5th item from a line, where + # items are separated by ':' instead of whitespace + +choose 2:5 # print everything from the 2nd to 5th item on the line, + # inclusive of the 5th + +choose -x 2:5 # print everything from the 2nd to 5th item on the line, + # exclusive of the 5th + +choose :3 # print the beginning of the line to the 3rd item + +choose -x :3 # print the beginning of the line to the 3rd item, + # exclusive + +choose 3: # print the third item to the end of the line + +choose -1 # print the last item from a line + +choose -3:-1 # print the last three items from a line +``` + +## Compilation and Installation + +### Installing From Source + +In order to build `choose` you will need the rust toolchain installed. You can +find instructions [here](https://www.rust-lang.org/tools/install). + +Then, to install: + +```bash +git clone https://github.com/theryangeary/choose.git +cd choose +cargo build --release +install target/release/choose +``` + +Just make sure DESTDIR is in your path. + +### Installing From Package Managers + +Cargo: + +```sh +cargo install choose +``` + +Arch Linux: + +```sh +yay -S choose-rust-git +``` + +Fedora/CentOS [COPR](https://copr.fedorainfracloud.org/coprs/atim/choose/): + +```sh +dnf copr enable atim/choose +dnf install choose +``` + +Homebrew: + +```sh +brew install choose-rust +``` + +MacPorts: + +```sh +sudo port install choose +``` + +### Benchmarking + +Benchmarking is performed using the [`bench` utility](https://github.com/Gabriel439/bench). + +Benchmarking is based on the assumption that there are five files in `test/` +that match the glob "long*txt". GitHub doesn't support files big enough in +normal repos, but for reference the files I'm working with have lengths like +these: + +```sh + 1000 test/long.txt + 19272 test/long_long.txt + 96360 test/long_long_long.txt + 963600 test/long_long_long_long.txt + 10599600 test/long_long_long_long_long.txt +``` + +and content generally like this: + +``` +Those an equal point no years do. Depend warmth fat but her but played. Shy and +subjects wondered trifling pleasant. Prudent cordial comfort do no on colonel as +assured chicken. Smart mrs day which begin. Snug do sold mr it if such. +Terminated uncommonly at at estimating. Man behaviour met moonlight extremity +acuteness direction. + +Ignorant branched humanity led now marianne too strongly entrance. Rose to shew +bore no ye of paid rent form. Old design are dinner better nearer silent excuse. +She which are maids boy sense her shade. Considered reasonable we affronting on +expression in. So cordial anxious mr delight. Shot his has must wish from sell +``` diff --git a/userland/upstream-src/choose/src/choice/mod.rs b/userland/upstream-src/choose/src/choice/mod.rs new file mode 100644 index 0000000000..c80bf4bf1c --- /dev/null +++ b/userland/upstream-src/choose/src/choice/mod.rs @@ -0,0 +1,263 @@ +use std::convert::TryInto; + +use crate::config::Config; +use crate::error::Error; +use crate::result::Result; +use crate::writeable::Writeable; +use crate::writer::WriteReceiver; + +#[cfg(test)] +mod test; + +#[derive(Debug)] +pub struct Choice { + pub start: isize, + pub end: isize, + pub kind: ChoiceKind, + negative_index: bool, + reversed: bool, +} + +#[derive(Debug, PartialEq)] +pub enum ChoiceKind { + Single, + RustExclusiveRange, + RustInclusiveRange, + ColonRange, +} + +impl Choice { + pub fn new(start: isize, end: isize, kind: ChoiceKind) -> Self { + let negative_index = start < 0 || end < 0; + let reversed = end < start && !(start >= 0 && end < 0); + Choice { + start, + end, + kind, + negative_index, + reversed, + } + } + + pub fn print_choice( + &self, + line: &str, + config: &Config, + handle: &mut W, + ) -> Result<()> { + if config.opt.character_wise { + self.print_choice_generic(line.chars(), config, handle) + } else { + let line_iter = config + .separator + .split(line) + .filter(|s| !s.is_empty() || config.opt.non_greedy); + self.print_choice_generic(line_iter, config, handle) + } + } + + pub fn is_reverse_range(&self) -> bool { + self.reversed + } + + pub fn has_negative_index(&self) -> bool { + self.negative_index + } + + fn print_choice_generic( + &self, + mut iter: I, + config: &Config, + handle: &mut W, + ) -> Result<()> + where + W: WriteReceiver, + T: Writeable, + I: Iterator, + { + if self.is_reverse_range() && !self.has_negative_index() { + self.print_choice_reverse(iter, config, handle)?; + } else if self.has_negative_index() { + self.print_choice_negative(iter, config, handle)?; + } else { + if self.start > 0 { + iter.nth((self.start - 1).try_into()?); + } + let range = self + .end + .checked_sub(self.start) + .ok_or_else(|| Error::Config("expected end > start".into()))?; + Choice::print_choice_loop_max_items(iter, config, handle, range)?; + } + + Ok(()) + } + + fn print_choice_loop_max_items( + iter: I, + config: &Config, + handle: &mut W, + max_items: isize, + ) -> Result<()> + where + W: WriteReceiver, + T: Writeable, + I: Iterator, + { + let mut peek_iter = iter.peekable(); + for i in 0..=max_items { + match peek_iter.next() { + Some(s) => { + handle.write_choice(s, config, peek_iter.peek().is_some() && i != max_items)?; + } + None => break, + }; + } + + Ok(()) + } + + /// Print choices that include at least one negative index + fn print_choice_negative(&self, iter: I, config: &Config, handle: &mut W) -> Result<()> + where + W: WriteReceiver, + T: Writeable, + I: Iterator, + { + let vec = iter.collect::>(); + + if let Some((start, end)) = self.get_negative_start_end(&vec)? { + if end > start { + for word in vec[start..std::cmp::min(end, vec.len() - 1)].iter() { + handle.write_choice(*word, config, true)?; + } + handle.write_choice(vec[std::cmp::min(end, vec.len() - 1)], config, false)?; + } else if self.start < 0 { + for word in vec[end + 1..=std::cmp::min(start, vec.len() - 1)] + .iter() + .rev() + { + handle.write_choice(*word, config, true)?; + } + handle.write_choice(vec[end], config, false)?; + } else if start == end && self.start < vec.len().try_into()? { + handle.write_choice(vec[start], config, false)?; + } + } + + Ok(()) + } + + fn print_choice_reverse( + &self, + mut iter: I, + config: &Config, + handle: &mut W, + ) -> Result<()> + where + W: WriteReceiver, + T: Writeable, + I: Iterator, + { + if self.end > 0 { + iter.nth((self.end - 1).try_into()?); + } + + let mut stack = Vec::new(); + for i in 0..=(self.start - self.end) { + match iter.next() { + Some(s) => stack.push(s), + None => break, + } + + if self.start <= self.end + i { + break; + } + } + + let mut peek_iter = stack.iter().rev().peekable(); + while let Some(s) = peek_iter.next() { + handle.write_choice(*s, config, peek_iter.peek().is_some())?; + } + + Ok(()) + } + + /// Get the absolute indexes of a choice range based on the slice length + /// + /// N.B. that this assumes that at least one index is negative - do not try to call this + /// function with a purely positive range. + /// + /// Returns Ok(None) if the resulting choice range would not include any item in the slice. + fn get_negative_start_end(&self, slice: &[T]) -> Result> { + if slice.is_empty() { + return Ok(None); + } + + let start_abs = self.start.checked_abs().ok_or_else(|| { + Error::Config(format!( + "Minimum index value supported is isize::MIN + 1 ({})", + isize::MIN + 1 + )) + })?; + + let slice_len_as_isize = slice.len().try_into()?; + + if self.kind == ChoiceKind::Single { + if start_abs <= slice_len_as_isize { + let idx = (slice_len_as_isize - start_abs).try_into()?; + Ok(Some((idx, idx))) + } else { + Ok(None) + } + } else { + let end_abs = self.end.checked_abs().ok_or_else(|| { + Error::Config(format!( + "Minimum index value supported is isize::MIN + 1 ({})", + isize::MIN + 1 + )) + })?; + + if self.start >= 0 { + // then we assume self.end is negative + let start = self.start.try_into()?; + + if end_abs <= slice_len_as_isize || start <= slice.len() || start > slice.len() { + let end = slice.len().saturating_sub(end_abs.try_into()?); + Ok(Some(( + std::cmp::min(start, slice.len().saturating_sub(1)), + std::cmp::min(end, slice.len().saturating_sub(1)), + ))) + } else { + Ok(None) + } + } else if self.end >= 0 { + // then we assume self.start is negative + let end = self.end.try_into()?; + + if start_abs <= slice_len_as_isize || end <= slice.len() || end > slice.len() { + let start = slice.len().saturating_sub(start_abs.try_into()?); + Ok(Some(( + std::cmp::min(start, slice.len().saturating_sub(1)), + std::cmp::min(end, slice.len().saturating_sub(1)), + ))) + } else { + Ok(None) + } + } else { + // both indices are negative + let start = slice.len().saturating_sub(start_abs.try_into()?); + let end = slice.len().saturating_sub(end_abs.try_into()?); + + if start_abs <= slice_len_as_isize || end_abs <= slice_len_as_isize { + Ok(Some(( + std::cmp::min(start, slice.len().saturating_sub(1)), + std::cmp::min(end, slice.len().saturating_sub(1)), + ))) + } else { + Ok(None) + } + } + } + } +} diff --git a/userland/upstream-src/choose/src/choice/test/get_negative_start_end.rs b/userland/upstream-src/choose/src/choice/test/get_negative_start_end.rs new file mode 100644 index 0000000000..edbc5bb680 --- /dev/null +++ b/userland/upstream-src/choose/src/choice/test/get_negative_start_end.rs @@ -0,0 +1,193 @@ +use super::*; +use crate::Error; + +#[test] +fn positive_negative_1() { + let config = Config::from_iter(vec!["choose", "2:-1"]); + let slice = &[1, 2, 3, 4, 5]; + assert_eq!( + Some((2, 4)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn positive_negative_gt1() { + let config = Config::from_iter(vec!["choose", "1:-3"]); + let slice = &[1, 2, 3, 4, 5]; + assert_eq!( + Some((1, 2)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn negative_positive() { + let config = Config::from_iter(vec!["choose", "-3:4"]); + let slice = &[1, 2, 3, 4, 5]; + assert_eq!( + Some((2, 4)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn negative_negative() { + let config = Config::from_iter(vec!["choose", "-3:-4"]); + let slice = &[1, 2, 3, 4, 5]; + assert_eq!( + Some((2, 1)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn negative1_negative1() { + let config = Config::from_iter(vec!["choose", "-1:-1"]); + let slice = &[1, 2, 3, 4, 5]; + assert_eq!( + Some((4, 4)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn negative_nonexisting_positive() { + let config = Config::from_iter(vec!["choose", "-3:9"]); + let slice = &[1, 2, 3, 4, 5]; + assert_eq!( + Some((2, 4)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn negative_negative_on_empty() { + let config = Config::from_iter(vec!["choose", "-3:-1"]); + let slice = &[0u8; 0]; + assert_eq!( + None, + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ) +} + +#[test] +fn negative_positive_on_empty() { + let config = Config::from_iter(vec!["choose", "-3:5"]); + let slice = &[0u8; 0]; + assert_eq!( + None, + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ) +} + +#[test] +fn positive_negative_on_empty() { + let config = Config::from_iter(vec!["choose", "2:-1"]); + let slice = &[0u8; 0]; + assert_eq!( + None, + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ) +} + +#[test] +fn negative_positive_all() { + let config = Config::from_iter(vec!["choose", "-5:9"]); + let slice = &[0, 1, 2, 3]; + assert_eq!( + Some((0, 3)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn negative_positive_some() { + let config = Config::from_iter(vec!["choose", "-5:2"]); + let slice = &[0, 1, 2, 3]; + assert_eq!( + Some((0, 2)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn positive_negative_all() { + let config = Config::from_iter(vec!["choose", "9:-5"]); + let slice = &[0, 1, 2, 3]; + assert_eq!( + Some((3, 0)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn positive_negative_some() { + let config = Config::from_iter(vec!["choose", "9:-2"]); + let slice = &[0, 1, 2, 3]; + assert_eq!( + Some((3, 2)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn positive_negative_same() { + let config = Config::from_iter(vec!["choose", "1:-3"]); + let slice = &[0, 1, 2, 3]; + assert_eq!( + Some((1, 1)), + config.opt.choices[0].get_negative_start_end(slice).unwrap() + ); +} + +#[test] +fn error_when_choice_is_isize_min() { + let isize_min = format!("{}", isize::MIN); + let config = Config::from_iter(vec!["choose", &isize_min]); + let slice = &[0, 1, 2, 3]; + + let err = config.opt.choices[0] + .get_negative_start_end(slice) + .unwrap_err(); + + if let Error::Config(s) = err { + assert!(s.contains("Minimum index value supported is isize::MIN")); + } else { + panic!("Expected Error::Config, found {}", err) + } +} + +#[test] +fn error_when_choice_start_is_isize_min() { + let choice = format!("{}:4", isize::MIN); + let config = Config::from_iter(vec!["choose", &choice]); + let slice = &[0, 1, 2, 3]; + + let err = config.opt.choices[0] + .get_negative_start_end(slice) + .unwrap_err(); + + if let Error::Config(s) = err { + assert!(s.contains("Minimum index value supported is isize::MIN")); + } else { + panic!("Expected Error::Config, found {}", err) + } +} + +#[test] +fn error_when_choice_end_is_isize_min() { + let choice = format!("4:{}", isize::MIN); + let config = Config::from_iter(vec!["choose", &choice]); + let slice = &[0, 1, 2, 3]; + + let err = config.opt.choices[0] + .get_negative_start_end(slice) + .unwrap_err(); + + if let Error::Config(s) = err { + assert!(s.contains("Minimum index value supported is isize::MIN")); + } else { + panic!("Expected Error::Config, found {}", err) + } +} diff --git a/userland/upstream-src/choose/src/choice/test/is_reverse_range.rs b/userland/upstream-src/choose/src/choice/test/is_reverse_range.rs new file mode 100644 index 0000000000..ae4d64ba2c --- /dev/null +++ b/userland/upstream-src/choose/src/choice/test/is_reverse_range.rs @@ -0,0 +1,31 @@ +use super::*; + +#[test] +fn is_field_reversed() { + let config = Config::from_iter(vec!["choose", "0"]); + assert_eq!(false, config.opt.choices[0].is_reverse_range()); +} + +#[test] +fn is_field_range_no_start_reversed() { + let config = Config::from_iter(vec!["choose", ":2"]); + assert_eq!(false, config.opt.choices[0].is_reverse_range()); +} + +#[test] +fn is_field_range_no_end_reversed() { + let config = Config::from_iter(vec!["choose", "2:"]); + assert_eq!(false, config.opt.choices[0].is_reverse_range()); +} + +#[test] +fn is_field_range_no_start_or_end_reversed() { + let config = Config::from_iter(vec!["choose", ":"]); + assert_eq!(false, config.opt.choices[0].is_reverse_range()); +} + +#[test] +fn is_reversed_field_range_reversed() { + let config = Config::from_iter(vec!["choose", "4:2"]); + assert_eq!(true, config.opt.choices[0].is_reverse_range()); +} diff --git a/userland/upstream-src/choose/src/choice/test/mod.rs b/userland/upstream-src/choose/src/choice/test/mod.rs new file mode 100644 index 0000000000..762804c73b --- /dev/null +++ b/userland/upstream-src/choose/src/choice/test/mod.rs @@ -0,0 +1,55 @@ +use crate::config::Config; +use crate::opt::Opt; +use std::ffi::OsString; +use std::io::{self, BufWriter, Write}; +use structopt::StructOpt; + +mod get_negative_start_end; +mod is_reverse_range; +mod print_choice; + +impl Config { + pub fn from_iter(iter: I) -> Self + where + I: IntoIterator, + I::Item: Into + Clone, + { + Config::new(Opt::from_iter(iter)) + } +} + +struct MockStdout { + pub buffer: String, +} + +impl MockStdout { + fn new() -> Self { + MockStdout { + buffer: String::new(), + } + } + + fn str_from_buf_writer(b: BufWriter) -> String { + match b.into_inner() { + Ok(b) => b.buffer, + Err(_) => panic!("Failed to access BufWriter inner writer"), + } + .trim_end() + .to_string() + } +} + +impl Write for MockStdout { + fn write(&mut self, buf: &[u8]) -> io::Result { + let mut bytes_written = 0; + for i in buf { + self.buffer.push(*i as char); + bytes_written += 1; + } + Ok(bytes_written) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/userland/upstream-src/choose/src/choice/test/print_choice.rs b/userland/upstream-src/choose/src/choice/test/print_choice.rs new file mode 100644 index 0000000000..63a63b5a28 --- /dev/null +++ b/userland/upstream-src/choose/src/choice/test/print_choice.rs @@ -0,0 +1,1032 @@ +use super::*; + +fn test_fn(vec: Vec<&str>, input: &str, output: &str) { + let config = Config::from_iter(vec); + let mut handle = BufWriter::new(MockStdout::new()); + + config.opt.choices[0] + .print_choice(&String::from(input), &config, &mut handle) + .unwrap(); + + assert_eq!( + String::from(output), + MockStdout::str_from_buf_writer(handle) + ); +} + +#[test] +fn print_0() { + test_fn(vec!["choose", "0"], "rust is pretty cool", "rust"); +} + +#[test] +fn print_after_end() { + test_fn(vec!["choose", "10"], "rust is pretty cool", ""); +} + +#[test] +fn print_out_of_order() { + let config = Config::from_iter(vec!["choose", "3", "1"]); + let mut handle = BufWriter::new(MockStdout::new()); + let mut handle1 = BufWriter::new(MockStdout::new()); + + config.opt.choices[0] + .print_choice(&String::from("rust is pretty cool"), &config, &mut handle) + .unwrap(); + + assert_eq!( + String::from("cool"), + MockStdout::str_from_buf_writer(handle) + ); + + config.opt.choices[1] + .print_choice(&String::from("rust is pretty cool"), &config, &mut handle1) + .unwrap(); + + assert_eq!(String::from("is"), MockStdout::str_from_buf_writer(handle1)); +} + +#[test] +fn print_1_to_3_exclusive() { + test_fn( + vec!["choose", "1:3", "-x"], + "rust is pretty cool", + "is pretty", + ); +} + +#[test] +fn print_1_to_3() { + test_fn( + vec!["choose", "1:3"], + "rust is pretty cool", + "is pretty cool", + ); +} + +#[test] +fn print_1_to_3_separated_by_hashtag() { + test_fn( + vec!["choose", "1:3", "-f", "#"], + "rust#is#pretty#cool", + "is pretty cool", + ); +} + +#[test] +fn print_1_to_3_separated_by_varying_multiple_hashtag_exclusive() { + test_fn( + vec!["choose", "1:3", "-f", "#", "-x"], + "rust##is###pretty####cool", + "is pretty", + ); +} + +#[test] +fn print_1_to_3_separated_by_varying_multiple_hashtag() { + test_fn( + vec!["choose", "1:3", "-f", "#"], + "rust##is###pretty####cool", + "is pretty cool", + ); +} + +#[test] +fn print_1_to_3_separated_by_regex_group_vowels_exclusive() { + test_fn( + vec!["choose", "1:3", "-f", "[aeiou]", "-x"], + "the quick brown fox jumped over the lazy dog", + " q ck br", + ); +} + +#[test] +fn print_1_to_3_separated_by_regex_group_vowels() { + test_fn( + vec!["choose", "1:3", "-f", "[aeiou]"], + "the quick brown fox jumped over the lazy dog", + " q ck br wn f", + ); +} + +#[test] +fn print_3_to_1() { + test_fn( + vec!["choose", "3:1"], + "rust lang is pretty darn cool", + "pretty is lang", + ); +} + +#[test] +fn print_3_to_1_exclusive() { + test_fn( + vec!["choose", "3:1", "-x"], + "rust lang is pretty darn cool", + "is lang", + ); +} + +#[test] +fn print_1_to_3_nonexistant_field_separator() { + test_fn( + vec!["choose", "1:3", "-f", "#"], + "rust lang is pretty darn cool", + "", + ); +} + +#[test] +fn print_0_nonexistant_field_separator() { + test_fn( + vec!["choose", "0", "-f", "#"], + "rust lang is pretty darn cool", + "rust lang is pretty darn cool", + ); +} + +#[test] +fn print_0_to_3_nonexistant_field_separator() { + test_fn( + vec!["choose", "0:3", "-f", "#"], + "rust lang is pretty darn cool", + "rust lang is pretty darn cool", + ); +} + +#[test] +fn print_0_with_preceding_separator() { + test_fn( + vec!["choose", "0"], + " rust lang is pretty darn cool", + "rust", + ); +} + +#[test] +fn print_neg3_to_neg1() { + test_fn( + vec!["choose", "-3:-1"], + "rust lang is pretty darn cool", + "pretty darn cool", + ); +} + +#[test] +fn print_neg1_to_neg3() { + test_fn( + vec!["choose", "-1:-3"], + "rust lang is pretty darn cool", + "cool darn pretty", + ); +} + +#[test] +fn print_neg2_to_end() { + test_fn( + vec!["choose", "-2:"], + "rust lang is pretty darn cool", + "darn cool", + ); +} + +#[test] +fn print_start_to_neg3() { + test_fn( + vec!["choose", ":-3"], + "rust lang is pretty darn cool", + "rust lang is pretty", + ); +} + +#[test] +fn print_1_to_neg3() { + test_fn( + vec!["choose", "1:-3"], + "rust lang is pretty darn cool", + "lang is pretty", + ); +} + +#[test] +fn print_5_to_neg3_empty() { + test_fn(vec!["choose", "5:-3"], "rust lang is pretty darn cool", ""); +} + +#[test] +fn print_0_to_2_greedy() { + test_fn(vec!["choose", "0:2", "-f", ":"], "a:b::c:::d", "a b c"); +} + +#[test] +fn print_0_to_2_non_greedy() { + test_fn(vec!["choose", "0:2", "-n", "-f", ":"], "a:b::c:::d", "a b"); +} + +#[test] +fn print_2_to_neg_1_non_greedy_negative() { + test_fn(vec!["choose", "2:-1", "-n", "-f", ":"], "a:b::c:::d", "c d"); +} + +#[test] +fn print_2_to_0_non_greedy_reversed() { + test_fn(vec!["choose", "2:0", "-n", "-f", ":"], "a:b::c:::d", "b a"); +} + +#[test] +fn print_neg_1_to_neg_3_non_greedy_negative_reversed() { + test_fn(vec!["choose", "-1:-3", "-n", "-f", ":"], "a:b::c:::d", "d"); +} + +#[test] +fn print_1_to_3_with_output_field_separator() { + test_fn(vec!["choose", "1:3", "-o", "#"], "a b c d", "b#c#d"); +} + +#[test] +fn print_1_and_3_with_output_field_separator() { + test_fn(vec!["choose", "1", "3", "-o", "#"], "a b c d", "b"); +} + +#[test] +fn print_2_to_4_with_output_field_separator() { + test_fn( + vec!["choose", "2:4", "-o", "%"], + "Lorem ipsum dolor sit amet, consectetur", + "dolor%sit%amet,", + ); +} + +#[test] +fn print_3_to_1_with_output_field_separator() { + test_fn(vec!["choose", "3:1", "-o", "#"], "a b c d", "d#c#b"); +} + +#[test] +fn print_0_to_neg_2_with_output_field_separator() { + test_fn(vec!["choose", "0:-2", "-o", "#"], "a b c d", "a#b#c"); +} + +#[test] +fn print_0_to_2_with_empty_output_field_separator() { + test_fn(vec!["choose", "0:2", "-o", ""], "a b c d", "abc"); +} + +#[test] +fn print_0_to_2_character_wise() { + test_fn(vec!["choose", "0:2", "-c"], "abcd", "abc"); +} + +#[test] +fn print_2_to_end_character_wise() { + test_fn(vec!["choose", "2:", "-c"], "abcd", "cd"); +} + +#[test] +fn print_start_to_2_character_wise() { + test_fn(vec!["choose", ":2", "-c"], "abcd", "abc"); +} + +#[test] +fn print_0_to_2_character_wise_exclusive() { + test_fn(vec!["choose", "0:2", "-c", "-x"], "abcd", "ab"); +} + +#[test] +fn print_0_to_2_character_wise_with_output_delimeter() { + test_fn(vec!["choose", "0:2", "-c", "-o", ":"], "abcd", "a:b:c"); +} + +#[test] +fn print_after_end_character_wise() { + test_fn(vec!["choose", "0:9", "-c"], "abcd", "abcd"); +} + +#[test] +fn print_2_to_0_character_wise() { + test_fn(vec!["choose", "2:0", "-c"], "abcd", "cba"); +} + +#[test] +fn print_neg_2_to_end_character_wise() { + test_fn(vec!["choose", "-2:", "-c"], "abcd", "cd"); +} + +#[test] +fn print_1_to_3_exclusive_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=3", "-x"], + "rust is pretty cool", + "is pretty cool", + ); +} + +#[test] +fn print_1_to_3_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=3"], + "rust is pretty cool", + "is pretty cool", + ); +} + +#[test] +fn print_1_to_3_separated_by_hashtag_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=3", "-f", "#"], + "rust#is#pretty#cool", + "is pretty cool", + ); +} + +#[test] +fn print_1_to_3_separated_by_varying_multiple_hashtag_exclusive_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=3", "-f", "#", "-x"], + "rust##is###pretty####cool", + "is pretty cool", + ); +} + +#[test] +fn print_1_to_3_separated_by_varying_multiple_hashtag_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=3", "-f", "#"], + "rust##is###pretty####cool", + "is pretty cool", + ); +} + +#[test] +fn print_1_to_3_separated_by_regex_group_vowels_exclusive_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=3", "-f", "[aeiou]", "-x"], + "the quick brown fox jumped over the lazy dog", + " q ck br wn f", + ); +} + +#[test] +fn print_1_to_3_separated_by_regex_group_vowels_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=3", "-f", "[aeiou]"], + "the quick brown fox jumped over the lazy dog", + " q ck br wn f", + ); +} + +#[test] +fn print_3_to_1_rust_syntax_inclusive() { + test_fn( + vec!["choose", "3..=1"], + "rust lang is pretty darn cool", + "pretty is lang", + ); +} + +#[test] +fn print_3_to_1_exclusive_rust_syntax_inclusive() { + test_fn( + vec!["choose", "3..=1", "-x"], + "rust lang is pretty darn cool", + "pretty is lang", + ); +} + +#[test] +fn print_1_to_3_nonexistant_field_separator_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=3", "-f", "#"], + "rust lang is pretty darn cool", + "", + ); +} + +#[test] +fn print_0_to_3_nonexistant_field_separator_rust_syntax_inclusive() { + test_fn( + vec!["choose", "0..=3", "-f", "#"], + "rust lang is pretty darn cool", + "rust lang is pretty darn cool", + ); +} + +#[test] +fn print_neg1_to_neg1_rust_syntax_inclusive() { + test_fn( + vec!["choose", "-3..=-1"], + "rust lang is pretty darn cool", + "pretty darn cool", + ); +} + +#[test] +fn print_neg1_to_neg3_rust_syntax_inclusive() { + test_fn( + vec!["choose", "-1..=-3"], + "rust lang is pretty darn cool", + "cool darn pretty", + ); +} + +#[test] +fn print_neg2_to_end_rust_syntax_inclusive() { + test_fn( + vec!["choose", "-2..="], + "rust lang is pretty darn cool", + "darn cool", + ); +} + +#[test] +fn print_start_to_neg3_rust_syntax_inclusive() { + test_fn( + vec!["choose", "..=-3"], + "rust lang is pretty darn cool", + "rust lang is pretty", + ); +} + +#[test] +fn print_1_to_neg3_rust_syntax_inclusive() { + test_fn( + vec!["choose", "1..=-3"], + "rust lang is pretty darn cool", + "lang is pretty", + ); +} + +#[test] +fn print_5_to_neg3_empty_rust_syntax_inclusive() { + test_fn( + vec!["choose", "5..=-3"], + "rust lang is pretty darn cool", + "", + ); +} + +#[test] +fn print_0_to_2_greedy_rust_syntax_inclusive() { + test_fn(vec!["choose", "0..=2", "-f", ":"], "a:b::c:::d", "a b c"); +} + +#[test] +fn print_0_to_2_non_greedy_rust_syntax_inclusive() { + test_fn( + vec!["choose", "0..=2", "-n", "-f", ":"], + "a:b::c:::d", + "a b", + ); +} + +#[test] +fn print_2_to_neg_1_non_greedy_negative_rust_syntax_inclusive() { + test_fn( + vec!["choose", "2..=-1", "-n", "-f", ":"], + "a:b::c:::d", + "c d", + ); +} + +#[test] +fn print_2_to_0_non_greedy_reversed_rust_syntax_inclusive() { + test_fn( + vec!["choose", "2..=0", "-n", "-f", ":"], + "a:b::c:::d", + "b a", + ); +} + +#[test] +fn print_neg_1_to_neg_3_non_greedy_negative_reversed_rust_syntax_inclusive() { + test_fn( + vec!["choose", "-1..=-3", "-n", "-f", ":"], + "a:b::c:::d", + "d", + ); +} + +#[test] +fn print_1_to_3_with_output_field_separator_rust_syntax_inclusive() { + test_fn(vec!["choose", "1..=3", "-o", "#"], "a b c d", "b#c#d"); +} + +#[test] +fn print_1_and_3_with_output_field_separator_rust_syntax_inclusive() { + let config = Config::from_iter(vec!["choose", "1", "3", "-o", "#"]); + let mut handle = BufWriter::new(MockStdout::new()); + config.opt.choices[0] + .print_choice(&String::from("a b c d"), &config, &mut handle) + .unwrap(); + handle.write(&config.output_separator).unwrap(); + config.opt.choices[1] + .print_choice(&String::from("a b c d"), &config, &mut handle) + .unwrap(); + assert_eq!(String::from("b#d"), MockStdout::str_from_buf_writer(handle)); +} + +#[test] +fn print_2_to_4_with_output_field_separator_rust_syntax_inclusive() { + test_fn( + vec!["choose", "2..=4", "-o", "%"], + "Lorem ipsum dolor sit amet, consectetur", + "dolor%sit%amet,", + ); +} + +#[test] +fn print_3_to_1_with_output_field_separator_rust_syntax_inclusive() { + test_fn(vec!["choose", "3..=1", "-o", "#"], "a b c d", "d#c#b"); +} + +#[test] +fn print_0_to_neg_2_with_output_field_separator_rust_syntax_inclusive() { + test_fn(vec!["choose", "0..=-2", "-o", "#"], "a b c d", "a#b#c"); +} + +#[test] +fn print_0_to_2_with_empty_output_field_separator_rust_syntax_inclusive() { + test_fn(vec!["choose", "0..=2", "-o", ""], "a b c d", "abc"); +} + +#[test] +fn print_0_to_2_character_wise_rust_syntax_inclusive() { + test_fn(vec!["choose", "0..=2", "-c"], "abcd", "abc"); +} + +#[test] +fn print_2_to_end_character_wise_rust_syntax_inclusive() { + test_fn(vec!["choose", "2..=", "-c"], "abcd", "cd"); +} + +#[test] +fn print_start_to_2_character_wise_rust_syntax_inclusive() { + test_fn(vec!["choose", "..=2", "-c"], "abcd", "abc"); +} + +#[test] +fn print_0_to_2_character_wise_exclusive_rust_syntax_inclusive() { + test_fn(vec!["choose", "0..=2", "-c", "-x"], "abcd", "abc"); +} + +#[test] +fn print_0_to_2_character_wise_with_output_delimeter_rust_syntax_inclusive() { + test_fn(vec!["choose", "0..=2", "-c", "-o", ":"], "abcd", "a:b:c"); +} + +#[test] +fn print_after_end_character_wise_rust_syntax_inclusive() { + test_fn(vec!["choose", "0..=9", "-c"], "abcd", "abcd"); +} + +#[test] +fn print_2_to_0_character_wise_rust_syntax_inclusive() { + test_fn(vec!["choose", "2..=0", "-c"], "abcd", "cba"); +} + +#[test] +fn print_neg_2_to_end_character_wise_rust_syntax_inclusive() { + test_fn(vec!["choose", "-2..=", "-c"], "abcd", "cd"); +} + +#[test] +fn print_1_to_3_exclusive_rust_syntax_exclusive() { + test_fn( + vec!["choose", "1..3", "-x"], + "rust is pretty cool", + "is pretty", + ); +} + +#[test] +fn print_1_to_3_rust_syntax_exclusive() { + test_fn(vec!["choose", "1..3"], "rust is pretty cool", "is pretty"); +} + +#[test] +fn print_1_to_3_separated_by_hashtag_rust_syntax_exclusive() { + test_fn( + vec!["choose", "1..3", "-f", "#"], + "rust#is#pretty#cool", + "is pretty", + ); +} + +#[test] +fn print_1_to_3_separated_by_varying_multiple_hashtag_exclusive_rust_syntax_exclusive() { + test_fn( + vec!["choose", "1..3", "-f", "#", "-x"], + "rust##is###pretty####cool", + "is pretty", + ); +} + +#[test] +fn print_1_to_3_separated_by_varying_multiple_hashtag_rust_syntax_exclusive() { + test_fn( + vec!["choose", "1..3", "-f", "#"], + "rust##is###pretty####cool", + "is pretty", + ); +} + +#[test] +fn print_1_to_3_separated_by_regex_group_vowels_exclusive_rust_syntax_exclusive() { + test_fn( + vec!["choose", "1..3", "-f", "[aeiou]", "-x"], + "the quick brown fox jumped over the lazy dog", + " q ck br", + ); +} + +#[test] +fn print_1_to_3_separated_by_regex_group_vowels_rust_syntax_exclusive() { + test_fn( + vec!["choose", "1..3", "-f", "[aeiou]"], + "the quick brown fox jumped over the lazy dog", + " q ck br", + ); +} + +#[test] +fn print_3_to_1_rust_syntax_exclusive() { + test_fn( + vec!["choose", "3..1"], + "rust lang is pretty darn cool", + "is lang", + ); +} + +#[test] +fn print_3_to_1_exclusive_rust_syntax_exclusive() { + test_fn( + vec!["choose", "3..1", "-x"], + "rust lang is pretty darn cool", + "is lang", + ); +} + +#[test] +fn print_1_to_3_nonexistant_field_separator_rust_syntax_exclusive() { + test_fn( + vec!["choose", "1..3", "-f", "#"], + "rust lang is pretty darn cool", + "", + ); +} + +#[test] +fn print_0_to_3_nonexistant_field_separator_rust_syntax_exclusive() { + test_fn( + vec!["choose", "0..3", "-f", "#"], + "rust lang is pretty darn cool", + "rust lang is pretty darn cool", + ); +} + +#[test] +fn print_neg3_to_neg1_rust_syntax_exclusive() { + test_fn( + vec!["choose", "-3..-1"], + "rust lang is pretty darn cool", + "pretty darn", + ); +} + +#[test] +fn print_neg1_to_neg3_rust_syntax_exclusive() { + test_fn( + vec!["choose", "-1..-3"], + "rust lang is pretty darn cool", + "darn pretty", + ); +} + +#[test] +fn print_neg2_to_end_rust_syntax_exclusive() { + test_fn( + vec!["choose", "-2.."], + "rust lang is pretty darn cool", + "darn cool", + ); +} + +#[test] +fn print_start_to_neg3_rust_syntax_exclusive() { + test_fn( + vec!["choose", "..-3"], + "rust lang is pretty darn cool", + "rust lang is", + ); +} + +#[test] +fn print_1_to_neg3_rust_syntax_exclusive() { + test_fn( + vec!["choose", "1..-3"], + "rust lang is pretty darn cool", + "lang is", + ); +} + +#[test] +fn print_5_to_neg3_empty_rust_syntax_exclusive() { + test_fn(vec!["choose", "5..-3"], "rust lang is pretty darn cool", ""); +} + +#[test] +fn print_0_to_2_greedy_rust_syntax_exclusive() { + test_fn(vec!["choose", "0..2", "-f", ":"], "a:b::c:::d", "a b"); +} + +#[test] +fn print_0_to_2_non_greedy_rust_syntax_exclusive() { + test_fn(vec!["choose", "0..2", "-n", "-f", ":"], "a:b::c:::d", "a b"); +} + +#[test] +fn print_2_to_neg_1_non_greedy_negative_rust_syntax_exclusive() { + test_fn(vec!["choose", "2..-1", "-n", "-f", ":"], "a:b::c:::d", "c"); +} + +#[test] +fn print_2_to_0_non_greedy_reversed_rust_syntax_exclusive() { + test_fn(vec!["choose", "2..0", "-n", "-f", ":"], "a:b::c:::d", "b a"); +} + +#[test] +fn print_neg_1_to_neg_3_non_greedy_negative_reversed_rust_syntax_exclusive() { + test_fn(vec!["choose", "-1..-3", "-n", "-f", ":"], "a:b::c:::d", ""); +} + +#[test] +fn print_1_to_3_with_output_field_separator_rust_syntax_exclusive() { + test_fn(vec!["choose", "1..3", "-o", "#"], "a b c d", "b#c"); +} + +#[test] +fn print_2_to_4_with_output_field_separator_rust_syntax_exclusive() { + test_fn( + vec!["choose", "2..4", "-o", "%"], + "Lorem ipsum dolor sit amet, consectetur", + "dolor%sit", + ); +} + +#[test] +fn print_3_to_1_with_output_field_separator_rust_syntax_exclusive() { + test_fn(vec!["choose", "3..1", "-o", "#"], "a b c d", "c#b"); +} + +#[test] +fn print_0_to_neg_2_with_output_field_separator_rust_syntax_exclusive() { + test_fn(vec!["choose", "0..-2", "-o", "#"], "a b c d", "a#b"); +} + +#[test] +fn print_0_to_2_with_empty_output_field_separator_rust_syntax_exclusive() { + test_fn(vec!["choose", "0..2", "-o", ""], "a b c d", "ab"); +} + +#[test] +fn print_0_to_2_character_wise_rust_syntax_exclusive() { + test_fn(vec!["choose", "0..2", "-c"], "abcd", "ab"); +} + +#[test] +fn print_2_to_end_character_wise_rust_syntax_exclusive() { + test_fn(vec!["choose", "2..", "-c"], "abcd", "cd"); +} + +#[test] +fn print_start_to_2_character_wise_rust_syntax_exclusive() { + test_fn(vec!["choose", "..2", "-c"], "abcd", "ab"); +} + +#[test] +fn print_0_to_2_character_wise_exclusive_rust_syntax_exclusive() { + test_fn(vec!["choose", "0..2", "-c", "-x"], "abcd", "ab"); +} + +#[test] +fn print_0_to_2_character_wise_with_output_delimeter_rust_syntax_exclusive() { + test_fn(vec!["choose", "0..2", "-c", "-o", ":"], "abcd", "a:b"); +} + +#[test] +fn print_after_end_character_wise_rust_syntax_exclusive() { + test_fn(vec!["choose", "0..9", "-c"], "abcd", "abcd"); +} + +#[test] +fn print_2_to_0_character_wise_rust_syntax_exclusive() { + test_fn(vec!["choose", "2..0", "-c"], "abcd", "ba"); +} + +#[test] +fn print_neg_2_to_end_character_wise_rust_syntax_exclusive() { + test_fn(vec!["choose", "-2..", "-c"], "abcd", "cd"); +} + +#[test] +fn print_2_exclusive() { + test_fn(vec!["choose", "2", "-x"], "a b c d", "c"); +} + +#[test] +fn print_2_one_indexed() { + test_fn(vec!["choose", "2", "--one-indexed"], "a b c d", "b"); +} + +#[test] +fn print_2_to_4_one_indexed() { + test_fn(vec!["choose", "2:4", "--one-indexed"], "a b c d", "b c d"); +} + +#[test] +fn print_2_to_end_one_indexed() { + test_fn(vec!["choose", "2:", "--one-indexed"], "a b c d", "b c d"); +} + +#[test] +fn print_start_to_2_one_indexed() { + test_fn(vec!["choose", ":2", "--one-indexed"], "a b c d", "a b"); +} + +#[test] +fn print_2_to_4_one_indexed_exclusive() { + test_fn( + vec!["choose", "2:4", "--one-indexed", "-x"], + "a b c d", + "b c", + ); +} + +#[test] +fn print_4_to_2_one_indexed() { + test_fn(vec!["choose", "4:2", "--one-indexed"], "a b c d", "d c b"); +} + +#[test] +fn print_neg_4_to_2_one_indexed() { + test_fn(vec!["choose", "-4:2", "--one-indexed"], "a b c d", "a b"); +} + +#[test] +fn print_2_to_4_newline_ofs() { + test_fn( + vec!["choose", "2:4", "-o", r#"\n"#], + "a b c d e f", + "c\nd\ne", + ); +} + +#[test] +fn print_negative_on_empty_line() { + test_fn(vec!["choose", "-1"], "", ""); +} + +#[test] +fn print_neg_2_when_field_empty() { + test_fn(vec!["choose", "-2"], "a", ""); +} + +#[test] +fn print_neg_4_to_neg_2_when_fields_empty() { + test_fn(vec!["choose", "-4:-2"], "a", ""); +} + +#[test] +fn print_neg_1_when_field_not_empty() { + test_fn(vec!["choose", "-1"], "a", "a"); +} + +#[test] +fn print_neg_2_when_field_not_empty() { + test_fn(vec!["choose", "-2"], "a b", "a"); +} + +#[test] +fn print_neg_4_to_neg_2_when_fields_not_empty() { + test_fn(vec!["choose", "-4:-2"], "a b c d e", "b c d"); +} + +#[test] +fn print_before_to_before_negative() { + test_fn(vec!["choose", "-8:-6"], "a b c d e", ""); +} + +#[test] +fn print_before_to_0() { + test_fn(vec!["choose", "-8:0"], "a b c d e", "a"); +} + +#[test] +fn print_before_to_middle() { + test_fn(vec!["choose", "-8:2"], "a b c d e", "a b c"); +} + +#[test] +fn print_before_to_after() { + test_fn(vec!["choose", "-6:10"], "a b c d e", "a b c d e"); +} + +#[test] +fn print_before_to_end_negative() { + test_fn(vec!["choose", "-6:-1"], "a b c d e", "a b c d e"); +} + +#[test] +fn print_middle_to_end_negative() { + test_fn(vec!["choose", "2:-1"], "a b c d e", "c d e"); +} + +#[test] +fn print_middle_to_after() { + test_fn(vec!["choose", "-3:10"], "a b c d e", "c d e"); +} + +#[test] +fn print_after_to_after() { + test_fn(vec!["choose", "10:10"], "a b c d e", ""); +} + +#[test] +fn print_negative_end_to_negative_end() { + test_fn(vec!["choose", "-1:-1"], "a b c d e", "e"); +} +////// +#[test] +fn print_before_to_before_negative_empty() { + test_fn(vec!["choose", "-8:-6"], "", ""); +} + +#[test] +fn print_before_to_0_empty() { + test_fn(vec!["choose", "-8:0"], "", ""); +} + +#[test] +fn print_before_to_middle_empty() { + test_fn(vec!["choose", "-8:2"], "", ""); +} + +#[test] +fn print_before_to_after_empty() { + test_fn(vec!["choose", "-6:10"], "", ""); +} + +#[test] +fn print_before_to_end_negative_empty() { + test_fn(vec!["choose", "-6:-1"], "", ""); +} + +#[test] +fn print_middle_to_end_negative_empty() { + test_fn(vec!["choose", "2:-1"], "", ""); +} + +#[test] +fn print_middle_to_after_empty() { + test_fn(vec!["choose", "-3:10"], "", ""); +} + +#[test] +fn print_after_to_after_empty() { + test_fn(vec!["choose", "10:10"], "", ""); +} + +#[test] +fn print_negative_end_to_negative_end_empty() { + test_fn(vec!["choose", "-1:-1"], "", ""); +} + +#[test] +fn print_positive_to_following_negative() { + test_fn(vec!["choose", "1:-3"], "a b c d e", "b c"); +} + +#[test] +fn print_positive_to_same_as_negative() { + test_fn(vec!["choose", "1:-4"], "a b c d e", "b"); +} + +#[test] +fn print_positive_to_preceding_negative() { + test_fn(vec!["choose", "1:-5"], "a b c d e", ""); +} + +#[test] +fn print_end_to_last_negative_is_last() { + test_fn(vec!["choose", "4:-1"], "a b c d e", "e"); +} + +#[test] +fn print_after_end_to_last_negative_is_empty() { + test_fn(vec!["choose", "5:-1"], "a b c d e", ""); +} + +#[test] +fn print_after_end_to_second_to_last_negative_is_empty() { + test_fn(vec!["choose", "5:-2"], "a b c d e", ""); +} diff --git a/userland/upstream-src/choose/src/config.rs b/userland/upstream-src/choose/src/config.rs new file mode 100644 index 0000000000..c2455c5900 --- /dev/null +++ b/userland/upstream-src/choose/src/config.rs @@ -0,0 +1,78 @@ +use regex::Regex; +use std::process; + +use crate::choice::ChoiceKind; +use crate::opt::Opt; + +pub struct Config { + pub opt: Opt, + pub separator: Regex, + pub output_separator: Box<[u8]>, +} + +impl Config { + pub fn new(mut opt: Opt) -> Self { + for choice in &mut opt.choices { + if (opt.exclusive && choice.kind == ChoiceKind::ColonRange) + || choice.kind == ChoiceKind::RustExclusiveRange + { + if choice.is_reverse_range() { + choice.start -= 1; + } else { + choice.end -= 1; + } + } + + if opt.one_indexed { + if choice.start > 0 { + choice.start -= 1; + } + + if choice.end > 0 { + choice.end -= 1; + } + } + } + + let separator = match Regex::new(match &opt.field_separator { + Some(s) => s, + None => "[[:space:]]", + }) { + Ok(r) => r, + Err(e) => { + // Exit code of 2 means failed to compile field_separator regex + match e { + regex::Error::Syntax(e) => { + eprintln!("Syntax error compiling regular expression: {}", e); + process::exit(2); + } + regex::Error::CompiledTooBig(e) => { + eprintln!("Compiled regular expression too big: compiled size cannot exceed {} bytes", e); + process::exit(2); + } + _ => { + eprintln!("Error compiling regular expression: {}", e); + process::exit(2); + } + } + } + }; + + let output_separator = match opt.character_wise { + false => match opt.output_field_separator.clone() { + Some(s) => s.into_boxed_str().into_boxed_bytes(), + None => Box::new([0x20; 1]), + }, + true => match opt.output_field_separator.clone() { + Some(s) => s.into_boxed_str().into_boxed_bytes(), + None => Box::new([]), + }, + }; + + Config { + opt, + separator, + output_separator, + } + } +} diff --git a/userland/upstream-src/choose/src/error.rs b/userland/upstream-src/choose/src/error.rs new file mode 100644 index 0000000000..f24fa3d8da --- /dev/null +++ b/userland/upstream-src/choose/src/error.rs @@ -0,0 +1,63 @@ +use std::error::Error as StdError; +use std::fmt; +use std::num::TryFromIntError; + +#[derive(Debug)] +pub enum Error { + Io(std::io::Error), + ParseRange(ParseRangeError), + TryFromInt(TryFromIntError), + Config(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Io(io) => write!(f, "{}", io), + Self::ParseRange(pr) => write!(f, "{}", pr), + Self::TryFromInt(tfi) => write!(f, "{}", tfi), + Self::Config(c) => write!(f, "{}", c), + } + } +} + +impl StdError for Error {} + +impl From for Error { + fn from(e: std::io::Error) -> Self { + Self::Io(e) + } +} + +impl From for Error { + fn from(e: ParseRangeError) -> Self { + Self::ParseRange(e) + } +} + +impl From for Error { + fn from(e: TryFromIntError) -> Self { + Self::TryFromInt(e) + } +} + +#[derive(Debug)] +pub struct ParseRangeError { + source_str: String, +} + +impl ParseRangeError { + pub fn new(source_str: &str) -> Self { + ParseRangeError { + source_str: String::from(source_str), + } + } +} + +impl fmt::Display for ParseRangeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.source_str) + } +} + +impl StdError for ParseRangeError {} diff --git a/userland/upstream-src/choose/src/escape.rs b/userland/upstream-src/choose/src/escape.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/userland/upstream-src/choose/src/escape.rs @@ -0,0 +1 @@ + diff --git a/userland/upstream-src/choose/src/main.rs b/userland/upstream-src/choose/src/main.rs new file mode 100644 index 0000000000..2139df57a1 --- /dev/null +++ b/userland/upstream-src/choose/src/main.rs @@ -0,0 +1,103 @@ +use std::fs::File; +use std::io::{self, Read}; +use std::process; +use structopt::StructOpt; + +#[macro_use] +extern crate lazy_static; + +mod choice; +mod config; +mod error; +mod escape; +mod opt; +mod parse; +mod parse_error; +mod reader; +mod result; +mod writeable; +mod writer; + +use config::Config; +use error::Error; +use opt::Opt; +use result::Result; +use writer::WriteReceiver; + +fn main() { + let opt = Opt::from_args(); + + let stdout = io::stdout(); + let lock = stdout.lock(); + let exit_result = match opt.input { + Some(_) => main_generic(opt, &mut io::BufWriter::new(lock)), + None => main_generic(opt, &mut io::LineWriter::new(lock)), + }; + + match exit_result { + Ok(_) => (), + Err(err) => { + match err { + Error::Io(e) => { + if e.kind() == io::ErrorKind::BrokenPipe { + // BrokenPipe means whoever is reading the output hung up, we should + // gracefully exit + } else { + eprintln!("Failed to write to output: {}", e) + } + } + e => eprintln!("Error: {}", e), + } + } + } +} + +fn main_generic(opt: Opt, handle: &mut W) -> Result<()> { + let config = Config::new(opt); + + let read = match &config.opt.input { + Some(f) => match File::open(f) { + Ok(fh) => Box::new(fh) as Box, + Err(e) => { + eprintln!("Failed to open file: {}", e); + // exit code of 3 means failure to open input file + process::exit(3); + } + }, + None => Box::new(io::stdin()) as Box, + }; + + let mut reader = reader::BufReader::new(read); + let mut buffer = String::new(); + + while let Some(line) = reader.read_line(&mut buffer) { + match line { + Ok(l) => { + let l = if (config.opt.character_wise || config.opt.field_separator.is_some()) + && l.ends_with('\n') + { + &l[0..l.len().saturating_sub(1)] + } else { + l + }; + + // trim end to remove newline or CRLF on windows + let l = l.trim_end(); + + let choice_iter = &mut config.opt.choices.iter().peekable(); + + while let Some(choice) = choice_iter.next() { + choice.print_choice(l, &config, handle)?; + if choice_iter.peek().is_some() { + handle.write_separator(&config)?; + } + } + + handle.write(b"\n").map(|_| ())? + } + Err(e) => println!("Failed to read line: {}", e), + } + } + + Ok(()) +} diff --git a/userland/upstream-src/choose/src/opt.rs b/userland/upstream-src/choose/src/opt.rs new file mode 100644 index 0000000000..3ce66f194c --- /dev/null +++ b/userland/upstream-src/choose/src/opt.rs @@ -0,0 +1,50 @@ +use std::path::PathBuf; +use structopt::StructOpt; + +use crate::choice::Choice; +use crate::parse; + +#[derive(Debug, StructOpt)] +#[structopt(name = "choose", about = "`choose` sections from each line of files")] +#[structopt(setting = structopt::clap::AppSettings::AllowLeadingHyphen)] +pub struct Opt { + /// Choose fields by character number + #[structopt(short, long)] + pub character_wise: bool, + + /// Activate debug mode + #[structopt(short, long)] + #[allow(unused)] + pub debug: bool, + + /// Use exclusive ranges, similar to array indexing in many programming languages + #[structopt(short = "x", long)] + pub exclusive: bool, + + /// Specify field separator other than whitespace, using Rust `regex` syntax + #[structopt(short, long)] + pub field_separator: Option, + + /// Input file + #[structopt(short, long, parse(from_os_str))] + pub input: Option, + + /// Use non-greedy field separators + #[structopt(short, long)] + pub non_greedy: bool, + + /// Index from 1 instead of 0 + #[structopt(long)] + pub one_indexed: bool, + + /// Specify output field separator + #[structopt(short, long, parse(from_str = parse::output_field_separator))] + pub output_field_separator: Option, + + /// Fields to print. Either a, a:b, a..b, or a..=b, where a and b are integers. The beginning + /// or end of a range can be omitted, resulting in including the beginning or end of the line, + /// respectively. a:b is inclusive of b (unless overridden by -x). a..b is + /// exclusive of b and a..=b is inclusive of b. + #[structopt(required = true, min_values = 1, parse(try_from_str = parse::choice))] + pub choices: Vec, +} diff --git a/userland/upstream-src/choose/src/parse.rs b/userland/upstream-src/choose/src/parse.rs new file mode 100644 index 0000000000..520d09c435 --- /dev/null +++ b/userland/upstream-src/choose/src/parse.rs @@ -0,0 +1,271 @@ +use backslash::escape_ascii; +use regex::Regex; + +use crate::choice::{Choice, ChoiceKind}; +use crate::error::ParseRangeError; +use crate::parse_error::ParseError; + +lazy_static! { + static ref PARSE_CHOICE_RE: Regex = Regex::new(r"^(-?\d*)(:|\.\.=?)(-?\d*)$").unwrap(); +} + +pub fn choice(src: &str) -> Result { + let cap = match PARSE_CHOICE_RE.captures_iter(src).next() { + Some(v) => v, + None => match src.parse() { + Ok(x) => return Ok(Choice::new(x, x, ChoiceKind::Single)), + Err(e) => { + eprintln!("failed to parse choice argument: {}", src); + return Err(ParseError::ParseIntError(e)); + } + }, + }; + + let start = if cap[1].is_empty() { + 0 + } else { + match cap[1].parse() { + Ok(x) => x, + Err(e) => { + eprintln!("failed to parse range start: {}", &cap[1]); + return Err(ParseError::ParseIntError(e)); + } + } + }; + + let kind = match &cap[2] { + ":" => ChoiceKind::ColonRange, + ".." => ChoiceKind::RustExclusiveRange, + "..=" => ChoiceKind::RustInclusiveRange, + _ => { + eprintln!( + "failed to parse range: not a valid range separator: {}", + &cap[2] + ); + return Err(ParseError::ParseRangeError(ParseRangeError::new(&cap[2]))); + } + }; + + let end = if cap[3].is_empty() { + isize::max_value() + } else { + match cap[3].parse() { + Ok(x) => x, + Err(e) => { + eprintln!("failed to parse range end: {}", &cap[3]); + return Err(ParseError::ParseIntError(e)); + } + } + }; + + Ok(Choice::new(start, end, kind)) +} + +pub fn output_field_separator(src: &str) -> String { + escape_ascii(src).unwrap() +} + +#[cfg(test)] +mod tests { + use crate::parse; + + mod parse_choice_tests { + use super::*; + + #[test] + fn parse_single_choice_start() { + let result = parse::choice("6").unwrap(); + assert_eq!(6, result.start) + } + + #[test] + fn parse_single_choice_end() { + let result = parse::choice("6").unwrap(); + assert_eq!(6, result.end) + } + + #[test] + fn parse_none_started_range() { + let result = parse::choice(":5").unwrap(); + assert_eq!((0, 5), (result.start, result.end)) + } + + #[test] + fn parse_none_terminated_range() { + let result = parse::choice("5:").unwrap(); + assert_eq!((5, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_full_range_pos_pos() { + let result = parse::choice("5:7").unwrap(); + assert_eq!((5, 7), (result.start, result.end)) + } + + #[test] + fn parse_full_range_neg_neg() { + let result = parse::choice("-3:-1").unwrap(); + assert_eq!((-3, -1), (result.start, result.end)) + } + + #[test] + fn parse_neg_started_none_ended() { + let result = parse::choice("-3:").unwrap(); + assert_eq!((-3, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_none_started_neg_ended() { + let result = parse::choice(":-1").unwrap(); + assert_eq!((0, -1), (result.start, result.end)) + } + + #[test] + fn parse_full_range_pos_neg() { + let result = parse::choice("5:-3").unwrap(); + assert_eq!((5, -3), (result.start, result.end)) + } + + #[test] + fn parse_full_range_neg_pos() { + let result = parse::choice("-3:5").unwrap(); + assert_eq!((-3, 5), (result.start, result.end)) + } + + #[test] + fn parse_beginning_to_end_range() { + let result = parse::choice(":").unwrap(); + assert_eq!((0, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_bad_choice() { + assert!(parse::choice("d").is_err()); + } + + #[test] + fn parse_bad_range() { + assert!(parse::choice("d:i").is_err()); + } + + #[test] + fn parse_rust_inclusive_range() { + let result = parse::choice("3..=5").unwrap(); + assert_eq!((3, 5), (result.start, result.end)) + } + + #[test] + fn parse_rust_inclusive_range_no_start() { + let result = parse::choice("..=5").unwrap(); + assert_eq!((0, 5), (result.start, result.end)) + } + + #[test] + fn parse_rust_inclusive_range_no_end() { + let result = parse::choice("3..=").unwrap(); + assert_eq!((3, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_rust_inclusive_range_no_start_or_end() { + let result = parse::choice("..=").unwrap(); + assert_eq!((0, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_full_range_pos_pos_rust_exclusive() { + let result = parse::choice("5..7").unwrap(); + assert_eq!((5, 7), (result.start, result.end)) + } + + #[test] + fn parse_full_range_neg_neg_rust_exclusive() { + let result = parse::choice("-3..-1").unwrap(); + assert_eq!((-3, -1), (result.start, result.end)) + } + + #[test] + fn parse_neg_started_none_ended_rust_exclusive() { + let result = parse::choice("-3..").unwrap(); + assert_eq!((-3, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_none_started_neg_ended_rust_exclusive() { + let result = parse::choice("..-1").unwrap(); + assert_eq!((0, -1), (result.start, result.end)) + } + + #[test] + fn parse_full_range_pos_neg_rust_exclusive() { + let result = parse::choice("5..-3").unwrap(); + assert_eq!((5, -3), (result.start, result.end)) + } + + #[test] + fn parse_full_range_neg_pos_rust_exclusive() { + let result = parse::choice("-3..5").unwrap(); + assert_eq!((-3, 5), (result.start, result.end)) + } + + #[test] + fn parse_rust_exclusive_range() { + let result = parse::choice("3..5").unwrap(); + assert_eq!((3, 5), (result.start, result.end)) + } + + #[test] + fn parse_rust_exclusive_range_no_start() { + let result = parse::choice("..5").unwrap(); + assert_eq!((0, 5), (result.start, result.end)) + } + + #[test] + fn parse_rust_exclusive_range_no_end() { + let result = parse::choice("3..").unwrap(); + assert_eq!((3, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_rust_exclusive_range_no_start_or_end() { + let result = parse::choice("..").unwrap(); + assert_eq!((0, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_full_range_pos_pos_rust_inclusive() { + let result = parse::choice("5..=7").unwrap(); + assert_eq!((5, 7), (result.start, result.end)) + } + + #[test] + fn parse_full_range_neg_neg_rust_inclusive() { + let result = parse::choice("-3..=-1").unwrap(); + assert_eq!((-3, -1), (result.start, result.end)) + } + + #[test] + fn parse_neg_started_none_ended_rust_inclusive() { + let result = parse::choice("-3..=").unwrap(); + assert_eq!((-3, isize::max_value()), (result.start, result.end)) + } + + #[test] + fn parse_none_started_neg_ended_rust_inclusive() { + let result = parse::choice("..=-1").unwrap(); + assert_eq!((0, -1), (result.start, result.end)) + } + + #[test] + fn parse_full_range_pos_neg_rust_inclusive() { + let result = parse::choice("5..=-3").unwrap(); + assert_eq!((5, -3), (result.start, result.end)) + } + + #[test] + fn parse_full_range_neg_pos_rust_inclusive() { + let result = parse::choice("-3..=5").unwrap(); + assert_eq!((-3, 5), (result.start, result.end)) + } + } +} diff --git a/userland/upstream-src/choose/src/parse_error.rs b/userland/upstream-src/choose/src/parse_error.rs new file mode 100644 index 0000000000..b49b4430d7 --- /dev/null +++ b/userland/upstream-src/choose/src/parse_error.rs @@ -0,0 +1,14 @@ +#[derive(Debug)] +pub enum ParseError { + ParseIntError(std::num::ParseIntError), + ParseRangeError(crate::error::ParseRangeError), +} + +impl ToString for ParseError { + fn to_string(&self) -> String { + match self { + ParseError::ParseIntError(e) => e.to_string(), + ParseError::ParseRangeError(e) => e.to_string(), + } + } +} diff --git a/userland/upstream-src/choose/src/reader.rs b/userland/upstream-src/choose/src/reader.rs new file mode 100644 index 0000000000..06349b7ab6 --- /dev/null +++ b/userland/upstream-src/choose/src/reader.rs @@ -0,0 +1,25 @@ +use std::io::{self, prelude::*}; + +pub struct BufReader { + reader: io::BufReader, +} + +impl BufReader { + pub fn new(f: R) -> Self { + Self { + reader: io::BufReader::new(f), + } + } + + pub fn read_line<'buf>( + &mut self, + buffer: &'buf mut String, + ) -> Option> { + buffer.clear(); + + self.reader + .read_line(buffer) + .map(|u| if u == 0 { None } else { Some(buffer) }) + .transpose() + } +} diff --git a/userland/upstream-src/choose/src/result.rs b/userland/upstream-src/choose/src/result.rs new file mode 100644 index 0000000000..8207680639 --- /dev/null +++ b/userland/upstream-src/choose/src/result.rs @@ -0,0 +1,3 @@ +use crate::error::Error; + +pub type Result = std::result::Result; diff --git a/userland/upstream-src/choose/src/writeable.rs b/userland/upstream-src/choose/src/writeable.rs new file mode 100644 index 0000000000..658b086cff --- /dev/null +++ b/userland/upstream-src/choose/src/writeable.rs @@ -0,0 +1,20 @@ +pub trait Writeable: Copy { + fn to_byte_buf(&self) -> Box<[u8]>; +} + +impl Writeable for &str { + fn to_byte_buf(&self) -> Box<[u8]> { + return Box::from(self.as_bytes()); + } +} + +impl Writeable for char { + fn to_byte_buf(&self) -> Box<[u8]> { + let mut buf = [0; 4]; + return self + .encode_utf8(&mut buf) + .to_owned() + .into_boxed_str() + .into_boxed_bytes(); + } +} diff --git a/userland/upstream-src/choose/src/writer.rs b/userland/upstream-src/choose/src/writer.rs new file mode 100644 index 0000000000..3b9e710248 --- /dev/null +++ b/userland/upstream-src/choose/src/writer.rs @@ -0,0 +1,27 @@ +use std::io::{self, BufWriter, LineWriter, Write}; + +use crate::config::Config; +use crate::writeable::Writeable; + +pub trait WriteReceiver: Write { + fn write_choice( + &mut self, + b: Wa, + config: &Config, + print_separator: bool, + ) -> io::Result<()> { + let num_bytes_written = self.write(&b.to_byte_buf())?; + if num_bytes_written > 0 && print_separator { + self.write_separator(config)?; + }; + Ok(()) + } + + fn write_separator(&mut self, config: &Config) -> io::Result<()> { + self.write(&config.output_separator).map(|_| ()) + } +} + +impl WriteReceiver for BufWriter {} + +impl WriteReceiver for LineWriter {} diff --git a/userland/upstream-src/csview/.cargo-ok b/userland/upstream-src/csview/.cargo-ok new file mode 100644 index 0000000000..5f8b795830 --- /dev/null +++ b/userland/upstream-src/csview/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/userland/upstream-src/csview/.cargo_vcs_info.json b/userland/upstream-src/csview/.cargo_vcs_info.json new file mode 100644 index 0000000000..fbcdbf02f2 --- /dev/null +++ b/userland/upstream-src/csview/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "4d36d2d736558517e2c0c6920dc5b1ff639b1f42" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/userland/upstream-src/csview/.gitattributes b/userland/upstream-src/csview/.gitattributes new file mode 100644 index 0000000000..febe51d98a --- /dev/null +++ b/userland/upstream-src/csview/.gitattributes @@ -0,0 +1 @@ +/completions/**/* linguist-generated diff --git a/userland/upstream-src/csview/.github/dependabot.yml b/userland/upstream-src/csview/.github/dependabot.yml new file mode 100644 index 0000000000..93d9249324 --- /dev/null +++ b/userland/upstream-src/csview/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: +- package-ecosystem: cargo + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + reviewers: + - wfxr + labels: + - dependencies + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" diff --git a/userland/upstream-src/csview/.github/issue_template.md b/userland/upstream-src/csview/.github/issue_template.md new file mode 100644 index 0000000000..bed735ff10 --- /dev/null +++ b/userland/upstream-src/csview/.github/issue_template.md @@ -0,0 +1,22 @@ + + + + +## Check list + +- [ ] I have read through the [README](https://github.com/wfxr/csview/blob/master/README.md) +- [ ] I have searched through the existing issues + +## Environment info + +- OS + - [ ] Linux + - [ ] Mac OS X + - [ ] Windows + - [ ] Others: + +## Version + + + +## Problem / Steps to reproduce diff --git a/userland/upstream-src/csview/.github/pull_request_template.md b/userland/upstream-src/csview/.github/pull_request_template.md new file mode 100644 index 0000000000..55b00908fe --- /dev/null +++ b/userland/upstream-src/csview/.github/pull_request_template.md @@ -0,0 +1,29 @@ + + +## Check list + +- [ ] I have read through the [README](https://github.com/wfxr/csview/blob/master/README.md) (especially F.A.Q section) +- [ ] I have searched through the existing issues or pull requests +- [ ] I have performed a self-review of my code and commented hard-to-understand areas +- [ ] I have made corresponding changes to the documentation (when necessary) + +## Description + + + +## Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor +- [ ] Breaking change +- [ ] Documentation change +- [ ] CICD related improvement + +## Test environment + +- OS + - [ ] Linux + - [ ] Mac OS X + - [ ] Windows + - [ ] Others: diff --git a/userland/upstream-src/csview/.github/workflows/CICD.yml b/userland/upstream-src/csview/.github/workflows/CICD.yml new file mode 100644 index 0000000000..a2a5785faa --- /dev/null +++ b/userland/upstream-src/csview/.github/workflows/CICD.yml @@ -0,0 +1,342 @@ +name: CICD + +env: + CICD_INTERMEDIATES_DIR: "_cicd-intermediates" + MSRV_FEATURES: "--all-features" + +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + tags: + - "*" + +jobs: + crate_metadata: + name: Extract crate metadata + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Extract crate information + id: crate_metadata + run: | + cargo metadata --no-deps --format-version 1 | jq -r '"name=" + .packages[0].name' | tee -a $GITHUB_OUTPUT + cargo metadata --no-deps --format-version 1 | jq -r '"version=" + .packages[0].version' | tee -a $GITHUB_OUTPUT + cargo metadata --no-deps --format-version 1 | jq -r '"maintainer=" + .packages[0].authors[0]' | tee -a $GITHUB_OUTPUT + cargo metadata --no-deps --format-version 1 | jq -r '"homepage=" + .packages[0].homepage' | tee -a $GITHUB_OUTPUT + cargo metadata --no-deps --format-version 1 | jq -r '"description=" + .packages[0].description' | tee -a $GITHUB_OUTPUT + cargo metadata --no-deps --format-version 1 | jq -r '"msrv=" + .packages[0].rust_version' | tee -a $GITHUB_OUTPUT + outputs: + name: ${{ steps.crate_metadata.outputs.name }} + version: ${{ steps.crate_metadata.outputs.version }} + maintainer: ${{ steps.crate_metadata.outputs.maintainer }} + homepage: ${{ steps.crate_metadata.outputs.homepage }} + description: ${{ steps.crate_metadata.outputs.description }} + msrv: ${{ steps.crate_metadata.outputs.msrv }} + + ensure_cargo_fmt: + name: Ensure 'cargo fmt' has been run + runs-on: ubuntu-latest + steps: + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - uses: actions/checkout@v4 + - run: cargo fmt -- --check + + # min_version: + # name: Minimum supported rust version + # runs-on: ubuntu-latest + # needs: crate_metadata + # steps: + # - name: Checkout source code + # uses: actions/checkout@v4 + # + # - name: Install rust toolchain (v${{ needs.crate_metadata.outputs.msrv }}) + # uses: dtolnay/rust-toolchain@master + # with: + # toolchain: ${{ needs.crate_metadata.outputs.msrv }} + # components: clippy + # - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix) + # run: cargo clippy --locked --all-targets ${{ env.MSRV_FEATURES }} + # - name: Run tests + # run: cargo test --locked ${{ env.MSRV_FEATURES }} + + build: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + needs: crate_metadata + strategy: + fail-fast: false + matrix: + job: + - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + - { target: aarch64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } + - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + - { target: i686-pc-windows-msvc , os: windows-2019 } + - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { target: x86_64-apple-darwin , os: macos-12 } + - { target: aarch64-apple-darwin , os: macos-14 } + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + - { target: x86_64-pc-windows-msvc , os: windows-2019 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + env: + BUILD_CMD: cargo + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install prerequisites + shell: bash + run: | + case ${{ matrix.job.target }} in + arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; + aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; + esac + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@nightly + with: + targets: ${{ matrix.job.target }} + + - name: Install cross + if: matrix.job.use-cross + uses: taiki-e/install-action@v2 + with: + tool: cross + + - name: Overwrite build command env variable + if: matrix.job.use-cross + shell: bash + run: echo "BUILD_CMD=cross" >> $GITHUB_ENV + + - name: Show version information (Rust, cargo, GCC) + shell: bash + run: | + gcc --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build + shell: bash + run: $BUILD_CMD build --locked --release --target=${{ matrix.job.target }} + + - name: Set binary name & path + id: bin + shell: bash + run: | + # Figure out suffix of binary + EXE_suffix="" + case ${{ matrix.job.target }} in + *-pc-windows-*) EXE_suffix=".exe" ;; + esac; + + # Setup paths + BIN_NAME="${{ needs.crate_metadata.outputs.name }}${EXE_suffix}" + BIN_PATH="target/${{ matrix.job.target }}/release/${BIN_NAME}" + + # Let subsequent steps know where to find the binary + echo "BIN_PATH=${BIN_PATH}" >> $GITHUB_OUTPUT + echo "BIN_NAME=${BIN_NAME}" >> $GITHUB_OUTPUT + + - name: Set testing options + id: test-options + shell: bash + run: | + # test only library unit tests and binary for arm-type targets + unset CARGO_TEST_OPTIONS + unset CARGO_TEST_OPTIONS ; case ${{ matrix.job.target }} in arm-* | aarch64-*) CARGO_TEST_OPTIONS="--bin ${{ needs.crate_metadata.outputs.name }}" ;; esac; + echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT + + - name: Run tests + shell: bash + run: $BUILD_CMD test --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}} + + - name: Test run + shell: bash + run: echo -e 'a,b,c\n1,2,3\n4,5,6\n7,8,9' | $BUILD_CMD run --locked --target=${{ matrix.job.target }} + + - name: Strip binary + shell: bash + run: | + if hash strip &>/dev/null; then + # strip binary if possible + strip "${{ steps.bin.outputs.BIN_PATH }}" || true + fi + + - name: Create tarball + id: package + shell: bash + run: | + PKG_suffix=".tar.gz" ; case ${{ matrix.job.target }} in *-pc-windows-*) PKG_suffix=".zip" ;; esac; + PKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-v${{ needs.crate_metadata.outputs.version }}-${{ matrix.job.target }} + PKG_NAME=${PKG_BASENAME}${PKG_suffix} + echo "PKG_NAME=${PKG_NAME}" >> $GITHUB_OUTPUT + + PKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/package" + ARCHIVE_DIR="${PKG_STAGING}/${PKG_BASENAME}/" + mkdir -p "${ARCHIVE_DIR}" + + # Binary + cp "${{ steps.bin.outputs.BIN_PATH }}" "$ARCHIVE_DIR" + + # README and LICENSE files + cp "README.md" "LICENSE-MIT" "LICENSE-APACHE" "$ARCHIVE_DIR" + + # Autocompletion files + cp -r completions "${ARCHIVE_DIR}" + + # base compressed package + pushd "${PKG_STAGING}/" >/dev/null + case ${{ matrix.job.target }} in + *-pc-windows-*) 7z -y a "${PKG_NAME}" "${PKG_BASENAME}"/* | tail -2 ;; + *) tar czf "${PKG_NAME}" "${PKG_BASENAME}"/* ;; + esac; + popd >/dev/null + + # Let subsequent steps know where to find the compressed package + echo "PKG_PATH=${PKG_STAGING}/${PKG_NAME}" >> $GITHUB_OUTPUT + + - name: Create Debian package + id: debian-package + shell: bash + if: startsWith(matrix.job.os, 'ubuntu') + run: | + COPYRIGHT_YEARS="2020 - "$(date "+%Y") + DPKG_STAGING="${{ env.CICD_INTERMEDIATES_DIR }}/debian-package" + DPKG_DIR="${DPKG_STAGING}/dpkg" + mkdir -p "${DPKG_DIR}" + + DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }} + DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }}-musl + case ${{ matrix.job.target }} in *-musl*) DPKG_BASENAME=${{ needs.crate_metadata.outputs.name }}-musl ; DPKG_CONFLICTS=${{ needs.crate_metadata.outputs.name }} ;; esac; + DPKG_VERSION=${{ needs.crate_metadata.outputs.version }} + + unset DPKG_ARCH + case ${{ matrix.job.target }} in + aarch64-*-linux-*) DPKG_ARCH=arm64 ;; + arm-*-linux-*hf) DPKG_ARCH=armhf ;; + i686-*-linux-*) DPKG_ARCH=i686 ;; + x86_64-*-linux-*) DPKG_ARCH=amd64 ;; + *) DPKG_ARCH=notset ;; + esac; + + DPKG_NAME="${DPKG_BASENAME}_${DPKG_VERSION}_${DPKG_ARCH}.deb" + echo "DPKG_NAME=${DPKG_NAME}" >> $GITHUB_OUTPUT + + # Binary + install -Dm755 "${{ steps.bin.outputs.BIN_PATH }}" "${DPKG_DIR}/usr/bin/${{ steps.bin.outputs.BIN_NAME }}" + + # Man page + # install -Dm644 'doc/${{ needs.crate_metadata.outputs.name }}.1' "${DPKG_DIR}/usr/share/man/man1/${{ needs.crate_metadata.outputs.name }}.1" + # gzip -n --best "${DPKG_DIR}/usr/share/man/man1/${{ needs.crate_metadata.outputs.name }}.1" + + # Autocompletion files + install -Dm644 'completions/bash/${{ needs.crate_metadata.outputs.name }}.bash' "${DPKG_DIR}/usr/share/bash-completion/completions/${{ needs.crate_metadata.outputs.name }}" + install -Dm644 'completions/fish/${{ needs.crate_metadata.outputs.name }}.fish' "${DPKG_DIR}/usr/share/fish/vendor_completions.d/${{ needs.crate_metadata.outputs.name }}.fish" + install -Dm644 'completions/zsh/_${{ needs.crate_metadata.outputs.name }}' "${DPKG_DIR}/usr/share/zsh/vendor-completions/_${{ needs.crate_metadata.outputs.name }}" + + # README and LICENSE + install -Dm644 "README.md" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/README.md" + install -Dm644 "LICENSE-MIT" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-MIT" + install -Dm644 "LICENSE-APACHE" "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/LICENSE-APACHE" + + cat > "${DPKG_DIR}/usr/share/doc/${DPKG_BASENAME}/copyright" < "${DPKG_DIR}/DEBIAN/control" <> $GITHUB_OUTPUT + + # build dpkg + fakeroot dpkg-deb --build "${DPKG_DIR}" "${DPKG_PATH}" + + - name: "Artifact upload: tarball" + uses: actions/upload-artifact@master + with: + name: ${{ steps.package.outputs.PKG_NAME }} + path: ${{ steps.package.outputs.PKG_PATH }} + + - name: "Artifact upload: Debian package" + uses: actions/upload-artifact@master + if: steps.debian-package.outputs.DPKG_NAME + with: + name: ${{ steps.debian-package.outputs.DPKG_NAME }} + path: ${{ steps.debian-package.outputs.DPKG_PATH }} + + - name: Check for release + id: is-release + shell: bash + run: | + unset IS_RELEASE ; if [[ $GITHUB_REF =~ ^refs/tags/v[0-9].* ]]; then IS_RELEASE='true' ; fi + echo "IS_RELEASE=${IS_RELEASE}" >> $GITHUB_OUTPUT + + - name: Publish archives and packages + uses: softprops/action-gh-release@v1 + if: steps.is-release.outputs.IS_RELEASE + with: + files: | + ${{ steps.package.outputs.PKG_PATH }} + ${{ steps.debian-package.outputs.DPKG_PATH }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/userland/upstream-src/csview/.github/workflows/release.yml b/userland/upstream-src/csview/.github/workflows/release.yml new file mode 100644 index 0000000000..118b953bec --- /dev/null +++ b/userland/upstream-src/csview/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + changelog: + name: Generate and publish changelog + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Generate a changelog + uses: orhun/git-cliff-action@v3 + id: git-cliff + with: + config: cliff.toml + args: -v --latest --strip header + env: + OUTPUT: CHANGES.md + GITHUB_REPO: ${{ github.repository }} + + - name: Polish changelog + shell: bash + run: sed -i '1,2d' CHANGES.md + + - name: Upload the changelog + uses: ncipollo/release-action@v1 + with: + # draft: true + allowUpdates: true + bodyFile: CHANGES.md diff --git a/userland/upstream-src/csview/.gitignore b/userland/upstream-src/csview/.gitignore new file mode 100644 index 0000000000..2c882fbd77 --- /dev/null +++ b/userland/upstream-src/csview/.gitignore @@ -0,0 +1,4 @@ +/target +/.idea +/flamegraph.svg +/perf.data diff --git a/userland/upstream-src/csview/.pre-commit-config.yaml b/userland/upstream-src/csview/.pre-commit-config.yaml new file mode 100644 index 0000000000..885e8446bb --- /dev/null +++ b/userland/upstream-src/csview/.pre-commit-config.yaml @@ -0,0 +1,43 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: check-yaml + - id: check-added-large-files + - id: mixed-line-ending + - id: check-toml +- repo: local + hooks: + - id: cargo-fmt + name: cargo fmt + pass_filenames: false + always_run: true + language: system + entry: cargo fmt + - id: cargo-check + name: cargo check + pass_filenames: false + always_run: true + language: system + entry: cargo check + - id: cargo-clippy + name: cargo clippy + pass_filenames: false + language: system + always_run: true + entry: cargo clippy + args: ["--", "-D", "warnings"] + - id: update-completions + name: update shell completions + pass_filenames: false + language: system + always_run: true + entry: > + sh -c ' + touch build.rs && + SHELL_COMPLETIONS_DIR=completions cargo build && + git add completions + ' diff --git a/userland/upstream-src/csview/CHANGELOG.md b/userland/upstream-src/csview/CHANGELOG.md new file mode 100644 index 0000000000..5d54c1f4ec --- /dev/null +++ b/userland/upstream-src/csview/CHANGELOG.md @@ -0,0 +1,357 @@ + +## [1.3.4](https://github.com/wfxr/csview/compare/v1.3.3..1.3.4) (2024-12-28) + +### 🚀 Features + +- Colored help message - ([d08efb0](https://github.com/wfxr/csview/commit/d08efb0b044bce4df81960ac1e877e2ca209c993)) + +### ⚙️ Miscellaneous Tasks + +- Ignore release commit message for cliff - ([70d956e](https://github.com/wfxr/csview/commit/70d956ea6de85909b7d935ca8febca9897d0d844)) + +## [1.3.3](https://github.com/wfxr/csview/compare/v1.3.2..v1.3.3) (2024-07-08) + +### 🐛 Bug Fixes + +- Workaround error caused by pager env - ([988add5](https://github.com/wfxr/csview/commit/988add57b9de99c61140c68fd868ee8bec79f5a2)) + +## [1.3.2](https://github.com/wfxr/csview/compare/v1.3.1..v1.3.2) (2024-04-30) + +### ⚙️ Miscellaneous Tasks + +- Add aarch64-apple-darwin target - ([4481363](https://github.com/wfxr/csview/commit/448136378a3edab2e1669efab63cd81cde20a703)) +- Update completions for new option - ([3565cfe](https://github.com/wfxr/csview/commit/3565cfee2dd62c4a22f5752ec261aa0b628e0a4a)) + +## [1.3.1](https://github.com/wfxr/csview/compare/v1.3.0..v1.3.1) (2024-04-29) + +### 🚀 Features + +- Only enable pager when stdout is a tty - ([420fd03](https://github.com/wfxr/csview/commit/420fd0310db32d42315fc5dcb2b745a4c4cc6909)) +- Allow disabling pager - ([80ae054](https://github.com/wfxr/csview/commit/80ae054283d857814408d186643105b57f7c559d)) + +### 📚 Documentation + +- Remove outdated faq - ([d814a38](https://github.com/wfxr/csview/commit/d814a389eec4a50ee6c0effc19ca601a37ff4395)) + +### ⚙️ Miscellaneous Tasks + +- Remove stale.yml - ([d3ac13b](https://github.com/wfxr/csview/commit/d3ac13b72a6af52cc81ab4482c1db44670fdb307)) + +## [1.3.0](https://github.com/wfxr/csview/compare/v1.2.4..v1.3.0) (2024-04-15) + +### 🚀 Features + +- Add pager support ([#198](https://github.com/wfxr/csview/issues/198)) - ([47d3c0a](https://github.com/wfxr/csview/commit/47d3c0a61b6e605ff2ebb0cd4d8d9a60adf758e2)) +- Show error when no input file specified - ([1214c96](https://github.com/wfxr/csview/commit/1214c962a6ff2d20eb1bb67fff3c49053406ca5f)) + +### 🚜 Refactor + +- Replace unmaintained `atty` ([#200](https://github.com/wfxr/csview/issues/200)) - ([d60612b](https://github.com/wfxr/csview/commit/d60612b669c41a2f4515f3a3cb067f3be185274e)) + +### ⚙️ Miscellaneous Tasks + +- Update dependencies - ([b9aed24](https://github.com/wfxr/csview/commit/b9aed24bc46e725a6de1c761e3eee2c508ed5338)) +- Fix release.toml - ([8b28b89](https://github.com/wfxr/csview/commit/8b28b89824b54fd110f9b73a24d8d839e8db627b)) +- Add rust-toolchain.toml & update rustfmt.toml ([#201](https://github.com/wfxr/csview/issues/201)) - ([6fc99d4](https://github.com/wfxr/csview/commit/6fc99d432a9a0670430beef1f74e8da5c126e547)) +- Add changelog ([#199](https://github.com/wfxr/csview/issues/199)) - ([a15325d](https://github.com/wfxr/csview/commit/a15325d1ac8d9d90172247b76bb8e3cc734da6d8)) +- Add FUNDING.yml - ([0e6045f](https://github.com/wfxr/csview/commit/0e6045fb85d2ca748c7dfd7f4e075fce0641bfe1)) + +## [1.2.4](https://github.com/wfxr/csview/compare/v1.2.3..v1.2.4) (2024-03-04) + +### ⚙️ Miscellaneous Tasks + +- Update cicd config ([#191](https://github.com/wfxr/csview/issues/191)) - ([3ea5126](https://github.com/wfxr/csview/commit/3ea512652476364ab2afcbae37d9432ce7695192)) + +## [1.2.3](https://github.com/wfxr/csview/compare/v1.2.2..v1.2.3) (2024-02-22) + +### ⚙️ Miscellaneous Tasks + +- Cargo update - ([d903e78](https://github.com/wfxr/csview/commit/d903e78363b850686d2a878ab101fe50f0a6e875)) +- Update stale config - ([e39d5a9](https://github.com/wfxr/csview/commit/e39d5a9972d0e742490c068dcc49781ab761e0af)) +- Bump up dependencies - ([4e8034f](https://github.com/wfxr/csview/commit/4e8034fda7a2f212898196bf444ff7cf1229ca87)) +- Avoid unnecessary ci jobs - ([0cec5ca](https://github.com/wfxr/csview/commit/0cec5cab52cb3e21fc1a3c70830b205302833660)) + +## [1.2.2](https://github.com/wfxr/csview/compare/v1.2.1..v1.2.2) (2022-10-09) + +### 🐛 Bug Fixes + +- Fix tests - ([9383041](https://github.com/wfxr/csview/commit/93830413d754f52b1ede1b7b0826f9e489720732)) + +### 🚜 Refactor + +- Adjust error code - ([cf101e9](https://github.com/wfxr/csview/commit/cf101e951a59f482b8271debf7bcef52c6273105)) +- Simplify sniff logic - ([9df90a5](https://github.com/wfxr/csview/commit/9df90a547969bcd9b1695ed5c019dffb84d9c916)) +- Simplify seq logic - ([14c1c88](https://github.com/wfxr/csview/commit/14c1c88aac405bcd961d846a8949502499a65b12)) + +### ⚙️ Miscellaneous Tasks + +- Upgrade clap to v4 - ([c0380e3](https://github.com/wfxr/csview/commit/c0380e306e272fa2cfd66d6394e2e643e1ac6f18)) +- Update stale config - ([4dc8ff1](https://github.com/wfxr/csview/commit/4dc8ff1b0692ba280b8dc8c9cd49f7c3712dfc59)) +- Refactor project layout - ([7b0c620](https://github.com/wfxr/csview/commit/7b0c62088e1179b0d160818c24a4baac5172d364)) + +## [1.2.1](https://github.com/wfxr/csview/compare/v1.2.0..v1.2.1) (2022-09-23) + +### 🚜 Refactor + +- Serialize enum in lowercase - ([1715587](https://github.com/wfxr/csview/commit/1715587a57fb27bf119fe959c5254ee5da21ab22)) + +## [1.2.0](https://github.com/wfxr/csview/compare/v1.1.0..v1.2.0) (2022-09-23) + +### 🚀 Features + +- Add alignment cli options - ([429eb5d](https://github.com/wfxr/csview/commit/429eb5d61cf23d5eee266dd06ee2559af280df6c)) +- Add alignment support - ([a01d227](https://github.com/wfxr/csview/commit/a01d2279f1b7b5ffccf04ee0da9a1398a6d30af8)) +- Add minimal ascii border style - ([0c5ff8e](https://github.com/wfxr/csview/commit/0c5ff8ecb85b50d3182af99891eac04b5cc55209)) + +### 📚 Documentation + +- Update example - ([af43fc9](https://github.com/wfxr/csview/commit/af43fc9dc9ffee47425e75643aee4df8742927b0)) + +### ⚙️ Miscellaneous Tasks + +- Bump up dependencies - ([d7ddc08](https://github.com/wfxr/csview/commit/d7ddc080fe2cdb5353b8372649e40dce30e5878e)) + +## [1.1.0](https://github.com/wfxr/csview/compare/v1.0.1..v1.1.0) (2022-07-02) + +### 🚀 Features + +- Support printing line numbers - ([95b31a5](https://github.com/wfxr/csview/commit/95b31a5700f6a4f460950f0f7baa89559d0c989b)) + +### 🚜 Refactor + +- Fix cargo clippy warnings - ([5a1e6d9](https://github.com/wfxr/csview/commit/5a1e6d9e353313a1ebd86cd01e712458e4c94321)) + +### 📚 Documentation + +- `csview` now in homebrew core repo - ([4fec934](https://github.com/wfxr/csview/commit/4fec9346aba10fc1b2ebfc797d83b8ce0f25a1ca)) + +### ⚙️ Miscellaneous Tasks + +- Bump up dependencies - ([ba43992](https://github.com/wfxr/csview/commit/ba43992ed2e1d8087b2f9b0f9d7a5f315b04a4da)) + +## [1.0.1](https://github.com/wfxr/csview/compare/v1.0.0..v1.0.1) (2022-02-16) + +### 📚 Documentation + +- Update readme - ([84a8392](https://github.com/wfxr/csview/commit/84a839265227182467fafc9ef285fb289d0b0eea)) + +### ⚙️ Miscellaneous Tasks + +- Remove unstable features - ([aca9261](https://github.com/wfxr/csview/commit/aca9261dd6d4e1f4245087537750b4969b82308f)) + +## [1.0.0](https://github.com/wfxr/csview/compare/v0.3.12..v1.0.0) (2022-02-16) + +### 🚀 Features + +- Change default style - ([2389b4a](https://github.com/wfxr/csview/commit/2389b4a75ce6b91c4188a375ba7bd6df98c3afaa)) +- Remove completion sub command - ([d29caba](https://github.com/wfxr/csview/commit/d29cababf42f9b930ef08a3af7b46eb47d78a4e6)) +- Add sniff limit option - ([56b4858](https://github.com/wfxr/csview/commit/56b485830165f8a093646b602cde55a37585e0cb)) +- Add padding and indent option - ([40d7085](https://github.com/wfxr/csview/commit/40d7085479eefe166b56858f19f62df83f0c34e6)) +- Skip title sep when there is no data - ([d53caa5](https://github.com/wfxr/csview/commit/d53caa5675094c33a673208f9ad26193058af781)) +- Replace `prettytable-rs` - ([081d965](https://github.com/wfxr/csview/commit/081d9658a038efea94ffc34f85d6903dac1f4ae4)) +- Implement table writer - ([9fdb7a6](https://github.com/wfxr/csview/commit/9fdb7a6903f0c6c85caea8990ad618893c8ca170)) + +### 🚜 Refactor + +- Rename & clean derive traits - ([d77a5b0](https://github.com/wfxr/csview/commit/d77a5b0fe5b10ebd9900c392585b314b0f1d09b2)) +- Remove cell - ([46bd2c4](https://github.com/wfxr/csview/commit/46bd2c414778b44155c20e778cba88c5784f978d)) +- Lint - ([cf5f340](https://github.com/wfxr/csview/commit/cf5f3406175fefc8a255af2b3fee6f658d146e1b)) +- Rename - ([573e297](https://github.com/wfxr/csview/commit/573e297c52695d12a61a9faec2e4bac946cadceb)) +- Remove redundant enums - ([863c97e](https://github.com/wfxr/csview/commit/863c97e74cd4f1f1b08711fe77e63b39b2d0a37c)) +- Rename - ([d6b336a](https://github.com/wfxr/csview/commit/d6b336a4c910e26563c4877fe809110c47cbc226)) +- Tweak cli options - ([b574e79](https://github.com/wfxr/csview/commit/b574e79b7b7d055ccd515a0ffa19ce4515a8e80c)) + +### 📚 Documentation + +- Update default style - ([bab68ce](https://github.com/wfxr/csview/commit/bab68ce17c106ffea523b522116a100132dad33a)) +- Fix time unit - ([b21642c](https://github.com/wfxr/csview/commit/b21642c5c550af87311bfc2e7ff78049d979f933)) +- Update - ([4d4461e](https://github.com/wfxr/csview/commit/4d4461e3ffec65dd2be0d6f1c87f6bd1b2c43fad)) +- Update - ([a83ad55](https://github.com/wfxr/csview/commit/a83ad555539bbe6a0e0768c01f9d0600f3bdc0cb)) + +### ⚡ Performance + +- Add buffer on writer - ([86c6101](https://github.com/wfxr/csview/commit/86c610119d506b0a09453c5f3b3a3a1223ae5cca)) +- Remove unnecessary buffer - ([ffc59ca](https://github.com/wfxr/csview/commit/ffc59ca90e2aa2c1fdcdc1c20e9114e693dc5c71)) + +### 🧪 Testing + +- Add no padding test - ([ca15128](https://github.com/wfxr/csview/commit/ca15128b1b2a0ec60e3f1e043946f87cbd96bb36)) + +### ⚙️ Miscellaneous Tasks + +- Bump up version - ([8cd76c7](https://github.com/wfxr/csview/commit/8cd76c73a75d0db1db7687d131d54d398f5b2967)) +- NextLineHelp - ([cb795c9](https://github.com/wfxr/csview/commit/cb795c95a8ba941e32127798ab515059b2f6d33b)) +- Switched to nightly rust - ([abb2300](https://github.com/wfxr/csview/commit/abb23008218c103cb6afba38c1381181b2d79908)) + +## [0.3.11](https://github.com/wfxr/csview/compare/v0.3.10..v0.3.11) (2022-01-01) + +### 🚜 Refactor + +- Misc - ([35bca57](https://github.com/wfxr/csview/commit/35bca57f605fa825d56d965eff1c7ea200d5c570)) + +### ⚙️ Miscellaneous Tasks + +- Update dependabot config - ([2d2cc6d](https://github.com/wfxr/csview/commit/2d2cc6d1ab09ac5349de0e426a6fc60ee267826c)) +- Clap v3 - ([569552b](https://github.com/wfxr/csview/commit/569552b57c5c5d922885aeeec515c67156b6c5ad)) + +## [0.3.10](https://github.com/wfxr/csview/compare/v0.3.9..v0.3.10) (2021-12-26) + +### 🚀 Features + +- Upgrade cli - ([2b40db4](https://github.com/wfxr/csview/commit/2b40db482fbe9b86fff74618590154941dd21939)) + +### 📚 Documentation + +- Update - ([7934cf7](https://github.com/wfxr/csview/commit/7934cf758b268c7dde0caa1b91d8b503414b18ff)) + +### ⚙️ Miscellaneous Tasks + +- Add rustfmt.toml - ([a0b9025](https://github.com/wfxr/csview/commit/a0b9025e930dbd947363e0149c0ffaa3332280e4)) + +### Typo + +- Misc - ([ce696ba](https://github.com/wfxr/csview/commit/ce696ba7ff2b89680b7bc4b9bb5bb145a86872f5)) + +## [0.3.9](https://github.com/wfxr/csview/compare/v0.3.8..v0.3.9) (2021-11-23) + +### 🚜 Refactor + +- Clippy lint - ([f36edb1](https://github.com/wfxr/csview/commit/f36edb1bdd49c6916d76d3f3687e4cde876c58e6)) + +### 📚 Documentation + +- Add pkgsrc installation. - ([9888062](https://github.com/wfxr/csview/commit/9888062d1b58de4f5a11ed05970c809dcab012a7)) + +### ⚙️ Miscellaneous Tasks + +- Update license - ([a1de726](https://github.com/wfxr/csview/commit/a1de726f18fd22de1c4d4111a56aee2328262b75)) +- Update ci config - ([33b2b94](https://github.com/wfxr/csview/commit/33b2b946833952eeda82fa04d6d92888f4434843)) +- Update linguist - ([a82edc9](https://github.com/wfxr/csview/commit/a82edc94c48ceab598bbf510481544763e0990e1)) +- Upgrade pre-commit version - ([26fe3d8](https://github.com/wfxr/csview/commit/26fe3d86b9bdf4e1497f3deb28d21677402cae03)) +- Upgrade to 2021 edition - ([0035c2e](https://github.com/wfxr/csview/commit/0035c2e6c636fc574e589dc5b00bfb6b0e5db3e0)) + +## [0.3.8](https://github.com/wfxr/csview/compare/v0.3.7..v0.3.8) (2021-04-05) + +### 🐛 Bug Fixes + +- Fix output when table is empty ([#8](https://github.com/wfxr/csview/issues/8)) - ([2fbe3fd](https://github.com/wfxr/csview/commit/2fbe3fd051babe2531bae92f6cca6a495841676e)) + +## [0.3.7](https://github.com/wfxr/csview/compare/v0.3.6..v0.3.7) (2021-03-19) + +### 🚜 Refactor + +- Improve error handling - ([67f7b44](https://github.com/wfxr/csview/commit/67f7b44dfa139ba56e24cf0b48001487fddfb0a9)) +- Error handling - ([b6c38ec](https://github.com/wfxr/csview/commit/b6c38ecd7f2060f5b906282324725f400ec8e26d)) + +### ⚙️ Miscellaneous Tasks + +- Add dependabot - ([f5e5920](https://github.com/wfxr/csview/commit/f5e5920677a5f4fc8317f0fc77645de681c68ea1)) + +## [0.3.6](https://github.com/wfxr/csview/compare/v0.3.5..v0.3.6) (2021-02-07) + +### 🚜 Refactor + +- Minor refactor & bump up dependencies - ([6edd84e](https://github.com/wfxr/csview/commit/6edd84e90056949916797b25f5aae56ddac8ed37)) + +### 📚 Documentation + +- Add install description for windows - ([06b3006](https://github.com/wfxr/csview/commit/06b3006971066b77a516b8c2e90ef4c306f6a781)) + +### ⚙️ Miscellaneous Tasks + +- Add .gitattributes - ([ca098b0](https://github.com/wfxr/csview/commit/ca098b0e5f9de1dd65993916220e683e9f131a13)) +- Update clippy hook - ([2aafe79](https://github.com/wfxr/csview/commit/2aafe7941bdc50e33a0d5d5d1427f1828e58b77b)) + +## [0.3.5](https://github.com/wfxr/csview/compare/v0.3.4..v0.3.5) (2020-09-23) + +### 🚀 Features + +- Add grid style - ([3b758a9](https://github.com/wfxr/csview/commit/3b758a9a406bede7e921dcb7c716ff3e2a5ac2b2)) + +### 📚 Documentation + +- Add macOS installation - ([985d643](https://github.com/wfxr/csview/commit/985d6439c9d79ac648e9930ee87895193d0550b7)) +- Fix table indent in README - ([5356a4d](https://github.com/wfxr/csview/commit/5356a4d462c4ffb85f3bb162bef192c5a0787d40)) +- Add arch linux installation - ([e0c178b](https://github.com/wfxr/csview/commit/e0c178b8f39b46b9a8dfdd9ba35e3f64206ac4ae)) + +## [0.3.4](https://github.com/wfxr/csview/compare/v0.3.3..v0.3.4) (2020-09-19) + +### 🚜 Refactor + +- Use exit code defined in sysexits.h - ([c52153e](https://github.com/wfxr/csview/commit/c52153e9f1c187953c663ee0e90c12bcc0d1c38c)) + +### 📚 Documentation + +- Misc - ([177f555](https://github.com/wfxr/csview/commit/177f555f17aff140319388b3ba57308a2ee47499)) +- Add installation section - ([320fd43](https://github.com/wfxr/csview/commit/320fd435f97206dffafe2238af99689e1e1b16df)) + +### ⚙️ Miscellaneous Tasks + +- Remove duplicated hooks - ([a0e3ca3](https://github.com/wfxr/csview/commit/a0e3ca3e85c0cab427eba54fcac7eae5a068cfc4)) +- Words - ([5691214](https://github.com/wfxr/csview/commit/5691214a66abf188e6c77f6a568dcddcc4947477)) +- Add probot-stale config - ([2f036c2](https://github.com/wfxr/csview/commit/2f036c25082e1e2eab1fc3577b3730e858302210)) +- Add issue/pr templates - ([4b1ba13](https://github.com/wfxr/csview/commit/4b1ba136ad9d5a537987d0cdec88eee64c9a8a6b)) + +## [0.3.3](https://github.com/wfxr/csview/compare/v0.3.2..v0.3.3) (2020-09-19) + +### 🚜 Refactor + +- Improve error handling ([#2](https://github.com/wfxr/csview/issues/2)) - ([511ac52](https://github.com/wfxr/csview/commit/511ac52313df9aa409a9f48142fb108d72ebb26a)) + +### 📚 Documentation + +- Add credits section - ([a037d45](https://github.com/wfxr/csview/commit/a037d455327d41d25fd15e3a27fc989d1560f024)) +- Add features section - ([7cf4aa6](https://github.com/wfxr/csview/commit/7cf4aa6ae126bbc117babd04f358beddd21fb4fc)) +- Add faq section - ([841bd55](https://github.com/wfxr/csview/commit/841bd55c63a5dc7e826ba4408192afb4f89c3710)) + +### ⚙️ Miscellaneous Tasks + +- Add about message - ([a2b2f9d](https://github.com/wfxr/csview/commit/a2b2f9dc50705c9fa19862e2b9501d6a2578240f)) + +### Lint + +- Cargo clippy - ([b1cb104](https://github.com/wfxr/csview/commit/b1cb104788062bc0e88c105eee5cb9275bbc8362)) + +## [0.3.2](https://github.com/wfxr/csview/compare/v0.3.1..v0.3.2) (2020-09-18) + +### 🚀 Features + +- Add markdown style - ([9fb038e](https://github.com/wfxr/csview/commit/9fb038e09e5501a7d5e51a704b99467f532dc18e)) + +### 📚 Documentation + +- Fix crates badge - ([a24e2be](https://github.com/wfxr/csview/commit/a24e2be7391029a1947c1e06565bd2f46341526b)) + +### ⚙️ Miscellaneous Tasks + +- Fix a typo ([#1](https://github.com/wfxr/csview/issues/1)) - ([a5e2e39](https://github.com/wfxr/csview/commit/a5e2e3998472f70b523bbc4886f932fea960b7a9)) + +## [0.3.1](https://github.com/wfxr/csview/compare/v0.3.0..v0.3.1) (2020-09-17) + +### 📚 Documentation + +- Add more descriptions - ([113609f](https://github.com/wfxr/csview/commit/113609f0590190dffcad799fa9862a1b0b9012e0)) +- Update readme - ([74c07f4](https://github.com/wfxr/csview/commit/74c07f419435ac7dbc5f87422695db787cd42c7d)) +- Update readme - ([906c747](https://github.com/wfxr/csview/commit/906c7474df7bd0860d4d72f35a1714a2fb76c46e)) +- Update help messages - ([1d48b23](https://github.com/wfxr/csview/commit/1d48b23eb18d52d2efe5bfb3236682a859a7ad1c)) + +## [0.3.0] - 2020-09-17 + +### 🚀 Features + +- Show possible values for shells - ([c2bbcbc](https://github.com/wfxr/csview/commit/c2bbcbca944d3fc27bd76314ae7ffed72c24d504)) +- Support border style - ([7be10ae](https://github.com/wfxr/csview/commit/7be10ae0148170e7ac6c3eca0797f9ae2bf83906)) +- Support tsv - ([d53fb02](https://github.com/wfxr/csview/commit/d53fb02bffab217b5cd1c55ff14defe694c73ded)) +- Add delimeter support - ([cc9a1b0](https://github.com/wfxr/csview/commit/cc9a1b075e5942e1c36f5b62077595263705c5fb)) +- Use prettytable to print - ([751980c](https://github.com/wfxr/csview/commit/751980cab7be9d9e4c4079835bb5b9a1b7ca30e7)) + +### 🚜 Refactor + +- Remove update command - ([3002149](https://github.com/wfxr/csview/commit/300214906649498b9713ebaa69cea7128ad43220)) + +### ⚙️ Miscellaneous Tasks + +- Add pre-commit hooks - ([cbc60d4](https://github.com/wfxr/csview/commit/cbc60d42287aef1e225961c33ff9a97f3a1c0a6d)) +- Ci & cd - ([985bd1e](https://github.com/wfxr/csview/commit/985bd1e2b9255e9a58709a376b7ce1b99f4f1fca)) + + diff --git a/userland/upstream-src/csview/Cargo.lock b/userland/upstream-src/csview/Cargo.lock new file mode 100644 index 0000000000..c35a177cf7 --- /dev/null +++ b/userland/upstream-src/csview/Cargo.lock @@ -0,0 +1,476 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cc" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" + +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_complete" +version = "4.5.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "csview" +version = "1.3.4" +dependencies = [ + "anyhow", + "clap", + "clap_complete", + "csv", + "exitcode", + "itertools", + "pager", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "pager" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2599211a5c97fbbb1061d3dc751fa15f404927e4846e07c643287d6d1f462880" +dependencies = [ + "errno 0.2.8", + "libc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags", + "errno 0.3.8", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "terminal_size" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-truncate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[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-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/userland/upstream-src/csview/Cargo.toml b/userland/upstream-src/csview/Cargo.toml new file mode 100644 index 0000000000..aa9a51cf29 --- /dev/null +++ b/userland/upstream-src/csview/Cargo.toml @@ -0,0 +1,86 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "csview" +version = "1.3.4" +authors = ["Wenxuan Zhang "] +build = "build.rs" +exclude = ["/completions"] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A high performance csv viewer with cjk/emoji support." +homepage = "https://github.com/wfxr/csview" +readme = "README.md" +keywords = [ + "csv", + "pager", + "viewer", + "tool", +] +categories = ["command-line-utilities"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/wfxr/csview" + +[profile.release] +lto = true +codegen-units = 1 + +[[bin]] +name = "csview" +path = "src/main.rs" + +[dependencies.anyhow] +version = "1.0" + +[dependencies.clap] +version = "4" +features = [ + "wrap_help", + "derive", +] + +[dependencies.csv] +version = "1.3" + +[dependencies.exitcode] +version = "1.1" + +[dependencies.itertools] +version = "0.13" + +[dependencies.unicode-truncate] +version = "2.0" + +[dependencies.unicode-width] +version = "0" + +[build-dependencies.clap] +version = "4" +features = [ + "wrap_help", + "derive", +] + +[build-dependencies.clap_complete] +version = "4" + +[features] +default = ["pager"] +pager = ["dep:pager"] + +[target.'cfg(target_family = "unix")'.dependencies.pager] +version = "0.16" +optional = true diff --git a/userland/upstream-src/csview/Cargo.toml.orig b/userland/upstream-src/csview/Cargo.toml.orig new file mode 100644 index 0000000000..420ccb5b19 --- /dev/null +++ b/userland/upstream-src/csview/Cargo.toml.orig @@ -0,0 +1,37 @@ +[package] +name = "csview" +version = "1.3.4" +authors = ["Wenxuan Zhang "] +description = "A high performance csv viewer with cjk/emoji support." +categories = ["command-line-utilities"] +homepage = "https://github.com/wfxr/csview" +keywords = ["csv", "pager", "viewer", "tool"] +readme = "README.md" +license = "MIT OR Apache-2.0" +exclude = ["/completions"] +repository = "https://github.com/wfxr/csview" +edition = "2021" +build = "build.rs" + +[features] +default = ["pager"] +pager = ["dep:pager"] + +[dependencies] +csv = "1.3" +clap = { version = "4", features = ["wrap_help", "derive"] } +exitcode = "1.1" +anyhow = "1.0" +unicode-width = "0" +unicode-truncate = "2.0" +itertools = "0.13" +[target.'cfg(target_family = "unix")'.dependencies] +pager = { version = "0.16", optional = true } + +[build-dependencies] +clap = { version = "4", features = ["wrap_help", "derive"] } +clap_complete = "4" + +[profile.release] +lto = true +codegen-units = 1 diff --git a/userland/upstream-src/csview/FUNDING.yml b/userland/upstream-src/csview/FUNDING.yml new file mode 100644 index 0000000000..63b4a7e420 --- /dev/null +++ b/userland/upstream-src/csview/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: wfxr diff --git a/userland/upstream-src/csview/LICENSE-APACHE b/userland/upstream-src/csview/LICENSE-APACHE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/userland/upstream-src/csview/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/userland/upstream-src/csview/LICENSE-MIT b/userland/upstream-src/csview/LICENSE-MIT new file mode 100644 index 0000000000..6eac644a2d --- /dev/null +++ b/userland/upstream-src/csview/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2020 Wenxuan Zhang (https://wfxr.mit-license.org/2020). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/userland/upstream-src/csview/README.md b/userland/upstream-src/csview/README.md new file mode 100644 index 0000000000..dc20ed7b3c --- /dev/null +++ b/userland/upstream-src/csview/README.md @@ -0,0 +1,190 @@ +

📠 csview

+

+ A high performance csv viewer with cjk/emoji support. +

+ +

+ + CICD + + License + + Version + + + Platform + +

+ + + +### Features + +* Small and *fast* (see [benchmarks](#benchmark) below). +* Memory efficient. +* Correctly align [CJK](https://en.wikipedia.org/wiki/CJK_characters) and emoji characters. +* Support `tsv` and custom delimiters. +* Support different styles, including markdown table. + +### Usage +``` +$ cat example.csv +Year,Make,Model,Description,Price +1997,Ford,E350,"ac, abs, moon",3000.00 +1999,Chevy,"Venture ""Extended Edition""","",4900.00 +1999,Chevy,"Venture ""Extended Edition, Large""",,5000.00 +1996,Jeep,Grand Cherokee,"MUST SELL! air, moon roof",4799.00 + +$ csview example.csv +┌──────┬───────┬───────────────────────────────────┬───────────────────────────┬─────────┐ +│ Year │ Make │ Model │ Description │ Price │ +├──────┼───────┼───────────────────────────────────┼───────────────────────────┼─────────┤ +│ 1997 │ Ford │ E350 │ ac, abs, moon │ 3000.00 │ +│ 1999 │ Chevy │ Venture "Extended Edition" │ │ 4900.00 │ +│ 1999 │ Chevy │ Venture "Extended Edition, Large" │ │ 5000.00 │ +│ 1996 │ Jeep │ Grand Cherokee │ MUST SELL! air, moon roof │ 4799.00 │ +└──────┴───────┴───────────────────────────────────┴───────────────────────────┴─────────┘ + +$ head /etc/passwd | csview -H -d: +┌────────────────────────┬───┬───────┬───────┬────────────────────────────┬─────────────────┐ +│ root │ x │ 0 │ 0 │ │ /root │ +│ bin │ x │ 1 │ 1 │ │ / │ +│ daemon │ x │ 2 │ 2 │ │ / │ +│ mail │ x │ 8 │ 12 │ │ /var/spool/mail │ +│ ftp │ x │ 14 │ 11 │ │ /srv/ftp │ +│ http │ x │ 33 │ 33 │ │ /srv/http │ +│ nobody │ x │ 65534 │ 65534 │ Nobody │ / │ +│ dbus │ x │ 81 │ 81 │ System Message Bus │ / │ +│ systemd-journal-remote │ x │ 981 │ 981 │ systemd Journal Remote │ / │ +│ systemd-network │ x │ 980 │ 980 │ systemd Network Management │ / │ +└────────────────────────┴───┴───────┴───────┴────────────────────────────┴─────────────────┘ +``` + +Run `csview --help` to view detailed usage. + +### Installation + +#### On Arch Linux + +`csview` is available in the Arch User Repository. To install it from [AUR](https://aur.archlinux.org/packages/csview): + +``` +yay -S csview +``` + +#### On macOS + +You can install `csview` with Homebrew: + +``` +brew install csview +``` + +#### On NetBSD + +`csview` is available from the main pkgsrc Repositories. To install simply run + +``` +pkgin install csview +``` + +or, if you prefer to build from source using [pkgsrc](https://pkgsrc.se/textproc/csview) on any of the supported platforms: + +``` +cd /usr/pkgsrc/textproc/csview +make install +``` + +#### On Windows + +You can install `csview` with [Scoop](https://scoop.sh/): +``` +scoop install csview +``` + +#### From binaries + +Pre-built versions of `csview` for various architectures are available at [Github release page](https://github.com/wfxr/csview/releases). + +*Note that you can try the `musl` version (which is statically-linked) if runs into dependency related errors.* + +#### From source + +`csview` is also published on [crates.io](https://crates.io). If you have latest Rust toolchains installed you can use `cargo` to install it from source: + +``` +cargo install --locked csview +``` + +If you want the latest version, clone this repository and run `cargo build --release`. + +### Benchmark + +- [small.csv](https://gist.github.com/wfxr/567e890d4db508b3c7630a96b703a57e#file-action-csv) (10 rows, 4 cols, 695 bytes): + +| Tool | Command | Mean Time | Min Time | Memory | +|:----------------------------------------------------------------------------------------:|---------------------------|----------:|----------:|----------:| +| [xsv](https://github.com/BurntSushi/xsv/tree/0.13.0) | `xsv table small.csv` | 2.0ms | 1.8ms | 3.9mb | +| [csview](https://github.com/wfxr/csview/tree/90ff90e26c3e4c4c37818d717555b3e8f90d27e3) | `csview small.csv` | **0.3ms** | **0.1ms** | **2.4mb** | +| [column](https://github.com/util-linux/util-linux/blob/stable/v2.37/text-utils/column.c) | `column -s, -t small.csv` | 1.3ms | 1.1ms | **2.4mb** | +| [csvlook](https://github.com/wireservice/csvkit/tree/1.0.6) | `csvlook small.csv` | 148.1ms | 142.4ms | 27.3mb | + +- [medium.csv](https://gist.github.com/wfxr/567e890d4db508b3c7630a96b703a57e#file-sample-csv) (10,000 rows, 10 cols, 624K bytes): + +| Tool | Command | Mean Time | Min Time | Memory | +|:----------------------------------------------------------------------------------------:|---------------------------|-----------:|-----------:|----------:| +| [xsv](https://github.com/BurntSushi/xsv/tree/0.13.0) | `xsv table medium.csv` | 0.031s | 0.029s | 4.4mb | +| [csview](https://github.com/wfxr/csview/tree/90ff90e26c3e4c4c37818d717555b3e8f90d27e3) | `csview medium.csv` | **0.017s** | **0.016s** | **2.8mb** | +| [column](https://github.com/util-linux/util-linux/blob/stable/v2.37/text-utils/column.c) | `column -s, -t small.csv` | 0.052s | 0.050s | 9.9mb | +| [csvlook](https://github.com/wireservice/csvkit/tree/1.0.6) | `csvlook medium.csv` | 2.664s | 2.617s | 46.8mb | + +- `large.csv` (1,000,000 rows, 10 cols, 61M bytes, generated by concatenating [medium.csv](https://gist.github.com/wfxr/567e890d4db508b3c7630a96b703a57e#file-sample-csv) 100 times): + +| Tool | Command | Mean Time | Min Time | Memory | +|:----------------------------------------------------------------------------------------:|---------------------------|-----------:|-----------:|----------:| +| [xsv](https://github.com/BurntSushi/xsv/tree/0.13.0) | `xsv table large.csv` | 2.912s | 2.820s | 4.4mb | +| [csview](https://github.com/wfxr/csview/tree/90ff90e26c3e4c4c37818d717555b3e8f90d27e3) | `csview large.csv` | **1.686s** | **1.665s** | **2.8mb** | +| [column](https://github.com/util-linux/util-linux/blob/stable/v2.37/text-utils/column.c) | `column -s, -t small.csv` | 5.777s | 5.759s | 767.6mb | +| [csvlook](https://github.com/wireservice/csvkit/tree/1.0.6) | `csvlook large.csv` | 20.665s | 20.549s | 1105.7mb | + +### F.A.Q. + +--- +#### We already have [xsv](https://github.com/BurntSushi/xsv), why not contribute to it but build a new tool? + +`xsv` is great. But it's aimed for analyzing and manipulating csv data. +`csview` is designed for formatting and viewing. See also: [xsv/issues/156](https://github.com/BurntSushi/xsv/issues/156) + +--- +#### I encountered UTF-8 related errors, how to solve it? + +The file may use a non-UTF8 encoding. You can check the file encoding using `file` command: + +``` +$ file -i a.csv +a.csv: application/csv; charset=iso-8859-1 +``` +And then convert it to `utf8`: + +``` +$ iconv -f iso-8859-1 -t UTF8//TRANSLIT a.csv -o b.csv +$ csview b.csv +``` + +Or do it in place: + +``` +$ iconv -f iso-8859-1 -t UTF8//TRANSLIT a.csv | csview +``` + +### Credits + +* [csv-rust](https://github.com/BurntSushi/rust-csv) +* [prettytable-rs](https://github.com/phsym/prettytable-rs) +* [structopt](https://github.com/TeXitoi/structopt) + +### License + +`csview` is distributed under the terms of both the MIT License and the Apache License 2.0. + +See the [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) files for license details. diff --git a/userland/upstream-src/csview/build.rs b/userland/upstream-src/csview/build.rs new file mode 100644 index 0000000000..7436e59460 --- /dev/null +++ b/userland/upstream-src/csview/build.rs @@ -0,0 +1,20 @@ +use clap::CommandFactory; +use clap_complete::Shell; +use std::{fs, path::Path}; + +include!("src/cli.rs"); + +fn main() -> Result<(), Box> { + let outdir = std::env::var_os("SHELL_COMPLETIONS_DIR") + .or_else(|| std::env::var_os("OUT_DIR")) + .expect("OUT_DIR not found"); + let outdir_path = Path::new(&outdir); + let app = &mut App::command(); + + for shell in Shell::value_variants() { + let dir = outdir_path.join(shell.to_string()); + fs::create_dir_all(&dir)?; + clap_complete::generate_to(*shell, app, app.get_name().to_string(), &dir)?; + } + Ok(()) +} diff --git a/userland/upstream-src/csview/cliff.toml b/userland/upstream-src/csview/cliff.toml new file mode 100644 index 0000000000..2809a1c8a3 --- /dev/null +++ b/userland/upstream-src/csview/cliff.toml @@ -0,0 +1,107 @@ +# git-cliff ~ configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. + +[changelog] +# changelog header +header = """ +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} + +{% macro print_commit(commit) -%} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }} - \ + ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ +{% endmacro -%} + +{% if version %}\ + {% if previous.version %}\ + ## [{{ version | trim_start_matches(pat="v") }}]\ + ({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) ({{ timestamp | date(format="%Y-%m-%d") }}) + {% else %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% endif %}\ +{% else %}\ + ## [unreleased] +{% endif %}\ + +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits + | filter(attribute="scope") + | sort(attribute="scope") %} + {{ self::print_commit(commit=commit) }} + {%- endfor -%} + {% raw %}\n{% endraw %}\ + {%- for commit in commits %} + {%- if not commit.scope -%} + {{ self::print_commit(commit=commit) }} + {% endif -%} + {% endfor -%} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing whitespace from the templates +trim = true +# postprocessors +postprocessors = [ + { pattern = '', replace = "https://github.com/wfxr/csview" }, # replace repository URL +] + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + { pattern = ' (#[0-9]+)', replace = '(${1})' }, + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "🚀 Features" }, + { message = "^fix", group = "🐛 Bug Fixes" }, + { message = "^doc", group = "📚 Documentation" }, + { message = "^perf", group = "⚡ Performance" }, + { message = "^refactor\\(clippy\\)", skip = true }, + { message = "^refactor", group = "🚜 Refactor" }, + { message = "^style", group = "🎨 Styling" }, + { message = "^test", group = "🧪 Testing" }, + { message = "^chore: [rR]elease", skip = true }, + { message = "^\\(cargo-release\\)", skip = true }, + { message = "^chore\\(deps.*\\)", skip = true }, + { message = "^chore\\(pr\\)", skip = true }, + { message = "^chore\\(pull\\)", skip = true }, + { message = "^chore|^ci", group = "⚙️ Miscellaneous Tasks" }, + { body = ".*security", group = "🛡️ Security" }, + { message = "^revert", group = "◀️ Revert" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out the commits that are not matched by commit parsers +filter_commits = false +# regex for matching git tags +tag_pattern = "v[0-9].*" +# regex for skipping tags +skip_tags = "beta|alpha" +# regex for ignoring tags +ignore_tags = "rc|v2.1.0|v2.1.1" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "newest" diff --git a/userland/upstream-src/csview/release.toml b/userland/upstream-src/csview/release.toml new file mode 100644 index 0000000000..c1e02354b0 --- /dev/null +++ b/userland/upstream-src/csview/release.toml @@ -0,0 +1,4 @@ +allow-branch = ["master"] +pre-release-hook = ["sh", "-c", "git cliff -o CHANGELOG.md --tag {{version}} && git add CHANGELOG.md"] +pre-release-commit-message = "chore: release {{crate_name}} version {{version}}" +tag-message = "chore: release {{crate_name}} version {{version}}" diff --git a/userland/upstream-src/csview/rust-toolchain.toml b/userland/upstream-src/csview/rust-toolchain.toml new file mode 100644 index 0000000000..3fe7418d20 --- /dev/null +++ b/userland/upstream-src/csview/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy", "rust-analyzer"] diff --git a/userland/upstream-src/csview/rustfmt.toml b/userland/upstream-src/csview/rustfmt.toml new file mode 100644 index 0000000000..0bcc3d2515 --- /dev/null +++ b/userland/upstream-src/csview/rustfmt.toml @@ -0,0 +1,13 @@ +edition = "2021" +max_width = 120 +reorder_imports = true +struct_lit_width = 60 +struct_variant_width = 60 + +# struct_field_align_threshold = 40 +# comment_width = 120 +# fn_single_line = false +# imports_layout = "HorizontalVertical" +# match_arm_blocks = false +# overflow_delimited_expr = true +# imports_granularity = "Crate" diff --git a/userland/upstream-src/csview/src/cli.rs b/userland/upstream-src/csview/src/cli.rs new file mode 100644 index 0000000000..95113ccba0 --- /dev/null +++ b/userland/upstream-src/csview/src/cli.rs @@ -0,0 +1,92 @@ +use std::path::PathBuf; + +use clap::ValueHint; +use clap::{ + builder::{ + styling::{AnsiColor, Effects}, + Styles, + }, + Parser, ValueEnum, +}; + +#[derive(Parser)] +#[clap(about, version)] +#[clap(disable_help_subcommand = true)] +#[clap(next_line_help = true)] +#[clap( + styles(Styles::styled() + .header(AnsiColor::Yellow.on_default() | Effects::BOLD) + .usage(AnsiColor::Yellow.on_default() | Effects::BOLD) + .literal(AnsiColor::Green.on_default() | Effects::BOLD) + .placeholder(AnsiColor::Cyan.on_default()) + ) +)] +pub struct App { + /// File to view. + #[arg(name = "FILE", value_hint = ValueHint::FilePath)] + pub file: Option, + + /// Specify that the input has no header row. + #[arg(short = 'H', long = "no-headers")] + pub no_headers: bool, + + /// Prepend a column of line numbers to the table. + #[arg(short, long, alias = "seq")] + pub number: bool, + + /// Use '\t' as delimiter for tsv. + #[arg(short, long, conflicts_with = "delimiter")] + pub tsv: bool, + + /// Specify the field delimiter. + #[arg(short, long, default_value_t = ',')] + pub delimiter: char, + + /// Specify the border style. + #[arg(short, long, value_enum, default_value_t = TableStyle::Sharp, ignore_case = true)] + pub style: TableStyle, + + /// Specify padding for table cell. + #[arg(short, long, default_value_t = 1)] + pub padding: usize, + + /// Specify global indent for table. + #[arg(short, long, default_value_t = 0)] + pub indent: usize, + + /// Limit column widths sniffing to the specified number of rows. Specify "0" to cancel limit. + #[arg(long, default_value_t = 1000, name = "LIMIT")] + pub sniff: usize, + + /// Specify the alignment of the table header. + #[arg(long, value_enum, default_value_t = Alignment::Center, ignore_case = true)] + pub header_align: Alignment, + + /// Specify the alignment of the table body. + #[arg(long, value_enum, default_value_t = Alignment::Left, ignore_case = true)] + pub body_align: Alignment, + + #[cfg(all(feature = "pager", unix))] + /// Disable pager. + #[arg(long, short = 'P')] + pub disable_pager: bool, +} + +#[derive(Copy, Clone, ValueEnum)] +pub enum TableStyle { + None, + Ascii, + Ascii2, + Sharp, + Rounded, + Reinforced, + Markdown, + Grid, +} + +#[derive(Copy, Clone, ValueEnum)] +pub enum Alignment { + Left, + Center, + Right, +} diff --git a/userland/upstream-src/csview/src/main.rs b/userland/upstream-src/csview/src/main.rs new file mode 100644 index 0000000000..8251e180d1 --- /dev/null +++ b/userland/upstream-src/csview/src/main.rs @@ -0,0 +1,102 @@ +mod cli; +mod table; +mod util; + +use anyhow::bail; +use clap::Parser; +use cli::App; +use csv::{ErrorKind, ReaderBuilder}; +use std::{ + fs::File, + io::{self, BufWriter, IsTerminal, Read}, + process, +}; +use table::TablePrinter; +use util::table_style; + +#[cfg(all(feature = "pager", unix))] +use pager::Pager; + +fn main() { + if let Err(e) = try_main() { + if let Some(ioerr) = e.root_cause().downcast_ref::() { + if ioerr.kind() == io::ErrorKind::BrokenPipe { + process::exit(exitcode::OK); + } + } + + if let Some(csverr) = e.root_cause().downcast_ref::() { + match csverr.kind() { + ErrorKind::Utf8 { .. } => { + eprintln!("[error] input is not utf8 encoded"); + process::exit(exitcode::DATAERR) + } + ErrorKind::UnequalLengths { pos, expected_len, len } => { + let pos_info = pos + .as_ref() + .map(|p| format!(" at (byte: {}, line: {}, record: {})", p.byte(), p.line(), p.record())) + .unwrap_or_else(|| "".to_string()); + eprintln!( + "[error] unequal lengths{}: expected length is {}, but got {}", + pos_info, expected_len, len + ); + process::exit(exitcode::DATAERR) + } + ErrorKind::Io(e) => { + eprintln!("[error] io error: {}", e); + process::exit(exitcode::IOERR) + } + e => { + eprintln!("[error] failed to process input: {:?}", e); + process::exit(exitcode::DATAERR) + } + } + } + + eprintln!("{}: {}", env!("CARGO_PKG_NAME"), e); + std::process::exit(1) + } +} + +fn try_main() -> anyhow::Result<()> { + let App { + file, + no_headers, + number, + tsv, + delimiter, + style, + padding, + indent, + sniff, + header_align, + body_align, + #[cfg(all(feature = "pager", unix))] + disable_pager, + } = App::parse(); + + #[cfg(all(feature = "pager", unix))] + if !disable_pager && io::stdout().is_terminal() { + match std::env::var("CSVIEW_PAGER") { + Ok(pager) => Pager::with_pager(&pager).setup(), + // XXX: the extra null byte can be removed once https://gitlab.com/imp/pager-rs/-/merge_requests/8 is merged + Err(_) => Pager::with_pager("less").pager_envs(["LESS=-SF\0"]).setup(), + } + } + + let stdout = io::stdout(); + let wtr = &mut BufWriter::new(stdout.lock()); + let rdr = ReaderBuilder::new() + .delimiter(if tsv { b'\t' } else { delimiter as u8 }) + .has_headers(!no_headers) + .from_reader(match file { + Some(path) => Box::new(File::open(path)?) as Box, + None if io::stdin().is_terminal() => bail!("no input file specified (use -h for help)"), + None => Box::new(io::stdin()), + }); + + let sniff = if sniff == 0 { usize::MAX } else { sniff }; + let table = TablePrinter::new(rdr, sniff, number)?; + table.writeln(wtr, &table_style(style, padding, indent, header_align, body_align))?; + Ok(()) +} diff --git a/userland/upstream-src/csview/src/table/mod.rs b/userland/upstream-src/csview/src/table/mod.rs new file mode 100644 index 0000000000..0a5513e81f --- /dev/null +++ b/userland/upstream-src/csview/src/table/mod.rs @@ -0,0 +1,6 @@ +mod printer; +mod row; +mod style; + +pub use printer::TablePrinter; +pub use style::{RowSep, Style, StyleBuilder}; diff --git a/userland/upstream-src/csview/src/table/printer.rs b/userland/upstream-src/csview/src/table/printer.rs new file mode 100644 index 0000000000..8c3e56a5a9 --- /dev/null +++ b/userland/upstream-src/csview/src/table/printer.rs @@ -0,0 +1,260 @@ +use super::{row::Row, style::Style}; +use csv::{Reader, StringRecord}; +use std::io::{self, Result, Write}; +use unicode_width::UnicodeWidthStr; + +pub struct TablePrinter { + header: Option, + widths: Vec, + records: Box>>, + with_seq: bool, +} + +impl TablePrinter { + pub(crate) fn new(mut rdr: Reader, sniff_rows: usize, with_seq: bool) -> Result { + let header = rdr.has_headers().then(|| rdr.headers()).transpose()?.cloned(); + let (widths, buf) = sniff_widths(&mut rdr, header.as_ref(), sniff_rows, with_seq)?; + let records = Box::new(buf.into_iter().map(Ok).chain(rdr.into_records())); + Ok(Self { header, widths, records, with_seq }) + } + + pub(crate) fn writeln(self, wtr: &mut W, fmt: &Style) -> Result<()> { + let widths = &self.widths; + fmt.rowseps + .top + .map(|sep| fmt.write_row_sep(wtr, widths, &sep)) + .transpose()?; + + let mut iter = self.records.peekable(); + + if let Some(header) = self.header { + let row: Row = self.with_seq.then_some("#").into_iter().chain(header.iter()).collect(); + row.writeln(wtr, fmt, widths, fmt.header_align)?; + if iter.peek().is_some() { + fmt.rowseps + .snd + .map(|sep| fmt.write_row_sep(wtr, widths, &sep)) + .transpose()?; + } + } + + let mut seq = 1; + while let Some(record) = iter.next().transpose()? { + let seq_str = self.with_seq.then(|| seq.to_string()); + let row: Row = seq_str.iter().map(|s| s.as_str()).chain(record.into_iter()).collect(); + row.writeln(wtr, fmt, widths, fmt.body_align)?; + if let Some(mid) = &fmt.rowseps.mid { + if iter.peek().is_some() { + fmt.write_row_sep(wtr, widths, mid)?; + } + } + seq += 1; + } + + fmt.rowseps + .bot + .map(|sep| fmt.write_row_sep(wtr, widths, &sep)) + .transpose()?; + + wtr.flush() + } +} + +fn sniff_widths( + rdr: &mut Reader, + header: Option<&StringRecord>, + sniff_rows: usize, + with_seq: bool, +) -> Result<(Vec, Vec)> { + let mut widths = Vec::new(); + let mut buf = Vec::new(); + + fn update_widths(record: &StringRecord, widths: &mut Vec) { + widths.resize(record.len(), 0); + record + .into_iter() + .map(UnicodeWidthStr::width_cjk) + .enumerate() + .for_each(|(i, width)| widths[i] = widths[i].max(width)) + } + + let mut record = header.cloned().unwrap_or_default(); + update_widths(&record, &mut widths); + + let mut seq = 1; + while seq <= sniff_rows && rdr.read_record(&mut record)? { + update_widths(&record, &mut widths); + seq += 1; + buf.push(record.clone()); + } + + if with_seq { + widths.insert(0, seq.to_string().width()); + } + Ok((widths, buf)) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::table::{RowSep, StyleBuilder}; + use anyhow::Result; + use csv::ReaderBuilder; + + macro_rules! gen_table { + ($($line:expr)*) => { + concat!( + $($line, "\n",)* + ) + }; + } + + #[test] + fn test_write() -> Result<()> { + let text = "a,b,c\n1,2,3\n4,5,6"; + let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes()); + let wtr = TablePrinter::new(rdr, 3, false)?; + + let mut buf = Vec::new(); + wtr.writeln(&mut buf, &Style::default())?; + + assert_eq!( + gen_table!( + "+---+---+---+" + "| a | b | c |" + "+---+---+---+" + "| 1 | 2 | 3 |" + "+---+---+---+" + "| 4 | 5 | 6 |" + "+---+---+---+" + ), + std::str::from_utf8(&buf)? + ); + Ok(()) + } + + #[test] + fn test_write_without_padding() -> Result<()> { + let text = "a,b,c\n1,2,3\n4,5,6"; + let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes()); + let wtr = TablePrinter::new(rdr, 3, false)?; + let fmt = StyleBuilder::default().padding(0).build(); + + let mut buf = Vec::new(); + wtr.writeln(&mut buf, &fmt)?; + + assert_eq!( + gen_table!( + "+-+-+-+" + "|a|b|c|" + "+-+-+-+" + "|1|2|3|" + "+-+-+-+" + "|4|5|6|" + "+-+-+-+" + ), + std::str::from_utf8(&buf)? + ); + Ok(()) + } + + #[test] + fn test_write_with_indent() -> Result<()> { + let text = "a,b,c\n1,2,3\n4,5,6"; + let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes()); + let wtr = TablePrinter::new(rdr, 3, false)?; + let fmt = StyleBuilder::default().indent(4).build(); + + let mut buf = Vec::new(); + wtr.writeln(&mut buf, &fmt)?; + + assert_eq!( + gen_table!( + " +---+---+---+" + " | a | b | c |" + " +---+---+---+" + " | 1 | 2 | 3 |" + " +---+---+---+" + " | 4 | 5 | 6 |" + " +---+---+---+" + ), + std::str::from_utf8(&buf)? + ); + Ok(()) + } + + #[test] + fn test_only_header() -> Result<()> { + let text = "a,ab,abc"; + let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes()); + let wtr = TablePrinter::new(rdr, 3, false)?; + let fmt = Style::default(); + + let mut buf = Vec::new(); + wtr.writeln(&mut buf, &fmt)?; + + assert_eq!( + gen_table!( + "+---+----+-----+" + "| a | ab | abc |" + "+---+----+-----+" + ), + std::str::from_utf8(&buf)? + ); + Ok(()) + } + + #[test] + fn test_without_header() -> Result<()> { + let text = "1,123,35\n383,2, 17"; + let rdr = ReaderBuilder::new().has_headers(false).from_reader(text.as_bytes()); + let wtr = TablePrinter::new(rdr, 3, false)?; + let fmt = StyleBuilder::new() + .col_sep('│') + .row_seps( + RowSep::new('─', '╭', '┬', '╮'), + RowSep::new('─', '├', '┼', '┤'), + None, + RowSep::new('─', '╰', '┴', '╯'), + ) + .build(); + + let mut buf = Vec::new(); + wtr.writeln(&mut buf, &fmt)?; + + assert_eq!( + gen_table!( + "╭─────┬─────┬─────╮" + "│ 1 │ 123 │ 35 │" + "│ 383 │ 2 │ 17 │" + "╰─────┴─────┴─────╯" + ), + std::str::from_utf8(&buf)? + ); + Ok(()) + } + + #[test] + fn test_with_seq() -> Result<()> { + let text = "a,b,c\n1,2,3\n4,5,6"; + let rdr = ReaderBuilder::new().has_headers(true).from_reader(text.as_bytes()); + let wtr = TablePrinter::new(rdr, 3, true)?; + + let mut buf = Vec::new(); + wtr.writeln(&mut buf, &Style::default())?; + + assert_eq!( + gen_table!( + "+---+---+---+---+" + "| # | a | b | c |" + "+---+---+---+---+" + "| 1 | 1 | 2 | 3 |" + "+---+---+---+---+" + "| 2 | 4 | 5 | 6 |" + "+---+---+---+---+" + ), + std::str::from_utf8(&buf)? + ); + Ok(()) + } +} diff --git a/userland/upstream-src/csview/src/table/row.rs b/userland/upstream-src/csview/src/table/row.rs new file mode 100644 index 0000000000..024f384b6e --- /dev/null +++ b/userland/upstream-src/csview/src/table/row.rs @@ -0,0 +1,96 @@ +use std::io::{Result, Write}; + +use itertools::Itertools; +use unicode_truncate::{Alignment, UnicodeTruncateStr}; + +use crate::table::Style; + +/// Represent a table row made of cells +#[derive(Clone, Debug)] +pub struct Row<'a> { + cells: Vec<&'a str>, +} + +impl<'a> FromIterator<&'a str> for Row<'a> { + fn from_iter>(iter: I) -> Self { + Self { cells: iter.into_iter().collect() } + } +} + +impl Row<'_> { + pub fn write(&self, wtr: &mut T, fmt: &Style, widths: &[usize], align: Alignment) -> Result<()> { + let sep = fmt.colseps.mid.map(|c| c.to_string()).unwrap_or_default(); + write!(wtr, "{:indent$}", "", indent = fmt.indent)?; + fmt.colseps.lhs.map(|sep| fmt.write_col_sep(wtr, sep)).transpose()?; + Itertools::intersperse( + self.cells + .iter() + .zip(widths) + .map(|(cell, &width)| cell.unicode_pad(width, align, true)) + .map(|s| format!("{:pad$}{}{:pad$}", "", s, "", pad = fmt.padding)), + sep, + ) + .try_for_each(|s| write!(wtr, "{}", s))?; + fmt.colseps.rhs.map(|sep| fmt.write_col_sep(wtr, sep)).transpose()?; + Ok(()) + } + + pub fn writeln(&self, wtr: &mut T, fmt: &Style, widths: &[usize], align: Alignment) -> Result<()> { + self.write(wtr, fmt, widths, align).and_then(|_| writeln!(wtr)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use anyhow::Result; + use std::str; + + #[test] + fn write_ascii_row() -> Result<()> { + let row = Row::from_iter(["a", "b"]); + let buf = &mut Vec::new(); + let fmt = Style::default(); + let widths = [3, 4]; + + row.writeln(buf, &fmt, &widths, Alignment::Left)?; + assert_eq!("| a | b |\n", str::from_utf8(buf)?); + Ok(()) + } + + #[test] + fn write_cjk_row() -> Result<()> { + let row = Row::from_iter(["李磊(Jack)", "四川省成都市", "💍"]); + let buf = &mut Vec::new(); + let fmt = Style::default(); + let widths = [10, 8, 2]; + + row.writeln(buf, &fmt, &widths, Alignment::Left)?; + assert_eq!("| 李磊(Jack) | 四川省成 | 💍 |\n", str::from_utf8(buf)?); + Ok(()) + } + + #[test] + fn write_align_center() -> Result<()> { + let row = Row::from_iter(["a", "b"]); + let buf = &mut Vec::new(); + let fmt = Style::default(); + let widths = [3, 4]; + + row.writeln(buf, &fmt, &widths, Alignment::Center)?; + assert_eq!("| a | b |\n", str::from_utf8(buf)?); + Ok(()) + } + + #[test] + fn write_align_right() -> Result<()> { + let row = Row::from_iter(["a", "b"]); + let buf = &mut Vec::new(); + let fmt = Style::default(); + let widths = [3, 4]; + + row.writeln(buf, &fmt, &widths, Alignment::Right)?; + assert_eq!("| a | b |\n", str::from_utf8(buf)?); + Ok(()) + } +} diff --git a/userland/upstream-src/csview/src/table/style.rs b/userland/upstream-src/csview/src/table/style.rs new file mode 100644 index 0000000000..9481941b8d --- /dev/null +++ b/userland/upstream-src/csview/src/table/style.rs @@ -0,0 +1,277 @@ +use std::io::{Result, Write}; + +use unicode_truncate::Alignment; + +#[derive(Debug, Clone, Copy)] +pub struct RowSeps { + /// Top separator row (top border) + /// ``` + /// >┌───┬───┐ + /// │ a │ b │ + /// ``` + pub top: Option, + /// Second separator row (between the header row and the first data row) + /// ``` + /// ┌───┬───┐ + /// │ a │ b │ + /// >├───┼───┤ + /// ``` + pub snd: Option, + /// Middle separator row (between data rows) + /// ``` + /// >├───┼───┤ + /// │ 2 │ 2 │ + /// >├───┼───┤ + /// ``` + pub mid: Option, + /// Bottom separator row (bottom border) + /// ``` + /// │ 3 │ 3 │ + /// >└───┴───┘ + /// ``` + pub bot: Option, +} + +/// The characters used for printing a row separator +#[derive(Debug, Clone, Copy)] +pub struct RowSep { + /// Inner row separator + /// ``` + /// ┌───┬───┐ + /// ^ + /// ``` + inner: char, + /// Left junction separator + /// ``` + /// ┌───┬───┐ + /// ^ + /// ``` + ljunc: char, + /// Crossing junction separator + /// ``` + /// ┌───┬───┐ + /// ^ + /// ``` + cjunc: char, + /// Right junction separator + /// ``` + /// ┌───┬───┐ + /// ^ + /// ``` + rjunc: char, +} + +#[derive(Debug, Clone, Copy)] +pub struct ColSeps { + /// Left separator column (left border) + /// ``` + /// │ 1 │ 2 │ + /// ^ + /// ``` + pub lhs: Option, + /// Middle column separators + /// ``` + /// │ 1 │ 2 │ + /// ^ + /// ``` + pub mid: Option, + /// Right separator column (right border) + /// ``` + /// │ 1 │ 2 │ + /// ^ + /// ``` + pub rhs: Option, +} + +impl RowSep { + pub fn new(sep: char, ljunc: char, cjunc: char, rjunc: char) -> RowSep { + RowSep { inner: sep, ljunc, cjunc, rjunc } + } +} + +impl Default for RowSep { + fn default() -> Self { + RowSep::new('-', '+', '+', '+') + } +} + +impl Default for RowSeps { + fn default() -> Self { + Self { + top: Some(RowSep::default()), + snd: Some(RowSep::default()), + mid: Some(RowSep::default()), + bot: Some(RowSep::default()), + } + } +} + +impl Default for ColSeps { + fn default() -> Self { + Self { lhs: Some('|'), mid: Some('|'), rhs: Some('|') } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Style { + /// Column style + pub colseps: ColSeps, + + /// Row style + pub rowseps: RowSeps, + + /// Left and right padding + pub padding: usize, + + /// Global indentation + pub indent: usize, + + /// Header alignment + pub header_align: Alignment, + + /// Data alignment + pub body_align: Alignment, +} + +impl Default for Style { + fn default() -> Self { + Self { + indent: 0, + padding: 1, + colseps: ColSeps::default(), + rowseps: RowSeps::default(), + header_align: Alignment::Center, + body_align: Alignment::Left, + } + } +} + +impl Style { + pub fn write_row_sep(&self, wtr: &mut W, widths: &[usize], sep: &RowSep) -> Result<()> { + write!(wtr, "{:indent$}", "", indent = self.indent)?; + if self.colseps.lhs.is_some() { + write!(wtr, "{}", sep.ljunc)?; + } + let mut iter = widths.iter().peekable(); + while let Some(width) = iter.next() { + for _ in 0..width + self.padding * 2 { + write!(wtr, "{}", sep.inner)?; + } + if self.colseps.mid.is_some() && iter.peek().is_some() { + write!(wtr, "{}", sep.cjunc)?; + } + } + if self.colseps.rhs.is_some() { + write!(wtr, "{}", sep.rjunc)?; + } + writeln!(wtr) + } + + #[inline] + pub fn write_col_sep(&self, wtr: &mut W, sep: char) -> Result<()> { + write!(wtr, "{}", sep) + } +} + +#[derive(Default, Debug, Clone)] +pub struct StyleBuilder { + format: Box"#).unwrap()); + +pub static START_TEMPLATE: Lazy = + Lazy::new(|| Regex::new(r#""#).unwrap()); +pub static END_TEMPLATE: Lazy = Lazy::new(|| Regex::new(r#""#).unwrap()); + +pub static STARTING_MARKDOWN_REGEX: Lazy = Lazy::new(|| Regex::new(r#"```\S+\s"#).unwrap()); +pub static ENDING_MARKDOWN_REGEX: Lazy = Lazy::new(|| Regex::new(r#"```\s?"#).unwrap()); + +pub static STARTING_LF_BLOCK_REGEX: Lazy = Lazy::new(|| Regex::new(r#"\{="#).unwrap()); +pub static ENDING_LF_BLOCK_REGEX: Lazy = Lazy::new(|| Regex::new(r#"=}"#).unwrap()); + +/// A memory of a regex matched. +/// The values provided by `Self::start` and `Self::end` are in the same space as the +/// start value supplied to `RegexCache::build` +pub struct Capture<'a> { + start: usize, + text: &'a [u8], +} + +impl Capture<'_> { + #[inline(always)] + fn start(&self) -> usize { + self.start + } + #[inline(always)] + pub fn end(&self) -> usize { + self.start + self.text.len() + } + #[inline(always)] + pub fn as_bytes(&self) -> &[u8] { + self.text + } +} + +impl<'a> std::fmt::Debug for Capture<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Capture") + .field("start", &self.start) + .field("end", &self.end()) + .field("text", &String::from_utf8_lossy(self.text)) + .finish() + } +} + +pub(crate) struct RegexCache<'a> { + inner: Option>, +} + +/// Embedding regexes are similar between different sets of languages. +/// `RegexFamily` records both which family the language belongs to, +/// as well as the actual matches +pub(crate) enum RegexFamily<'a> { + HtmlLike(HtmlLike<'a>), + LinguaFranca(SimpleCapture<'a>), + Markdown(SimpleCapture<'a>), + Rust, +} + +pub(crate) struct HtmlLike<'a> { + start_script: Option]>>, + start_style: Option]>>, + start_template: Option]>>, +} + +pub(crate) struct SimpleCapture<'a> { + starts: Option]>>, +} + +impl<'a> HtmlLike<'a> { + pub fn start_script_in_range<'this>( + &'this self, + start: usize, + end: usize, + ) -> Option>> { + filter_range(self.start_script.as_ref()?, start, end) + } + + pub fn start_style_in_range<'this>( + &'this self, + start: usize, + end: usize, + ) -> Option>> { + filter_range(self.start_style.as_ref()?, start, end) + } + + pub fn start_template_in_range<'this>( + &'this self, + start: usize, + end: usize, + ) -> Option>> { + filter_range(self.start_template.as_ref()?, start, end) + } +} + +impl<'a> SimpleCapture<'a> { + pub fn starts_in_range<'this>( + &'this self, + start: usize, + end: usize, + ) -> Option<&'this Capture<'a>> { + filter_range(self.starts.as_ref()?, start, end).and_then(|mut it| it.next()) + } + + fn make_capture( + regex: &Regex, + lines: &'a [u8], + start: usize, + end: usize, + ) -> Option> { + let capture = SimpleCapture { + starts: save_captures(regex, lines, start, end), + }; + + if capture.starts.is_some() { + Some(capture) + } else { + None + } + } +} + +fn filter_range<'dataset, 'cap>( + dataset: &'dataset [Capture<'cap>], + start: usize, + end: usize, +) -> Option>> { + let pos = dataset + .binary_search_by_key(&start, |cap| cap.start()) + .ok()?; + + if pos >= dataset.len() || dataset[pos].end() > end { + None + } else { + Some( + dataset[pos..] + .iter() + .take_while(move |cap| cap.end() <= end), + ) + } +} + +impl<'a> RegexCache<'a> { + /// Returns the language family for which regexes were matched, if any + pub(crate) fn family(&self) -> Option<&RegexFamily> { + self.inner.as_ref() + } + + /// Tries to memoize any matches of embedding regexes that occur within lines[start..end] + /// for the given language. Any `Capture` values eventually recovered will use the same + /// zero for their start as the given `start` argument. + pub(crate) fn build(lang: LanguageType, lines: &'a [u8], start: usize, end: usize) -> Self { + let inner = match lang { + LanguageType::Markdown | LanguageType::UnrealDeveloperMarkdown => { + SimpleCapture::make_capture(&STARTING_MARKDOWN_REGEX, lines, start, end) + .map(RegexFamily::Markdown) + } + LanguageType::Rust => Some(RegexFamily::Rust), + LanguageType::LinguaFranca => { + SimpleCapture::make_capture(&STARTING_LF_BLOCK_REGEX, lines, start, end) + .map(RegexFamily::LinguaFranca) + } + LanguageType::Html + | LanguageType::RubyHtml + | LanguageType::Svelte + | LanguageType::Vue + | LanguageType::GlimmerJs + | LanguageType::GlimmerTs => { + let html = HtmlLike { + start_script: save_captures(&START_SCRIPT, lines, start, end), + start_style: save_captures(&START_STYLE, lines, start, end), + start_template: save_captures(&START_TEMPLATE, lines, start, end), + }; + + if html.start_script.is_some() + || html.start_style.is_some() + || html.start_template.is_some() + { + Some(RegexFamily::HtmlLike(html)) + } else { + None + } + } + _ => None, + }; + Self { inner } + } +} + +fn save_captures<'a>( + regex: &Regex, + lines: &'a [u8], + start: usize, + end: usize, +) -> Option]>> { + let v: Vec<_> = regex + .captures(&lines[start..end])? + .iter() + .flatten() + .map(|cap| Capture { + start: start + cap.start(), + text: cap.as_bytes(), + }) + .collect(); + + if v.is_empty() { + None + } else { + Some(v.into()) + } +} diff --git a/userland/upstream-src/tokei/src/language/language_type.rs b/userland/upstream-src/tokei/src/language/language_type.rs new file mode 100644 index 0000000000..bffdf7a87a --- /dev/null +++ b/userland/upstream-src/tokei/src/language/language_type.rs @@ -0,0 +1,355 @@ +use std::{ + borrow::Cow, + fmt, + fs::File, + io::{self, Read}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use crate::{ + config::Config, + language::syntax::{FileContext, LanguageContext, SyntaxCounter}, + stats::{CodeStats, Report}, + utils::{ext::SliceExt, fs as fsutils}, +}; + +use encoding_rs_io::DecodeReaderBytesBuilder; +use grep_searcher::{LineIter, LineStep}; +use once_cell::sync::Lazy; +use rayon::prelude::*; +use serde::Serialize; + +use self::LanguageType::*; + +include!(concat!(env!("OUT_DIR"), "/language_type.rs")); + +impl Serialize for LanguageType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.name()) + } +} + +impl LanguageType { + /// Parses a given [`Path`] using the [`LanguageType`]. Returning [`Report`] + /// on success and giving back ownership of [`PathBuf`] on error. + pub fn parse(self, path: PathBuf, config: &Config) -> Result { + let text = { + let f = match File::open(&path) { + Ok(f) => f, + Err(e) => return Err((e, path)), + }; + let mut s = Vec::new(); + let mut reader = DecodeReaderBytesBuilder::new().build(f); + + if let Err(e) = reader.read_to_end(&mut s) { + return Err((e, path)); + } + s + }; + + let mut stats = Report::new(path); + + stats += self.parse_from_slice(text, config); + + Ok(stats) + } + + /// Parses the text provided as the given [`LanguageType`]. + pub fn parse_from_str>(self, text: A, config: &Config) -> CodeStats { + self.parse_from_slice(text.as_ref().as_bytes(), config) + } + + /// Parses the bytes provided as the given [`LanguageType`]. + pub fn parse_from_slice>(self, text: A, config: &Config) -> CodeStats { + let text = text.as_ref(); + + if self == Jupyter { + return self + .parse_jupyter(text.as_ref(), config) + .unwrap_or_default(); + } + + let syntax = { + let mut syntax_mut = SyntaxCounter::new(self); + if self == LinguaFranca { + syntax_mut.lf_embedded_language = self.find_lf_target_language(text); + } + syntax_mut + }; + + if let Some(end) = syntax.shared.important_syntax.find(text).and_then(|m| { + // Get the position of the last line before the important + // syntax. + text[..=m.start()] + .iter() + .rev() + .position(|&c| c == b'\n') + .filter(|&p| p != 0) + .map(|p| m.start() - p) + }) { + let (skippable_text, rest) = text.split_at(end + 1); + let is_fortran = syntax.shared.is_fortran; + let is_literate = syntax.shared.is_literate; + let comments = syntax.shared.line_comments; + trace!( + "Using Simple Parse on {:?}", + String::from_utf8_lossy(skippable_text) + ); + let parse_lines = move || self.parse_lines(config, rest, CodeStats::new(), syntax); + let simple_parse = move || { + LineIter::new(b'\n', skippable_text) + .par_bridge() + .map(|line| { + // FORTRAN has a rule where it only counts as a comment if it's the + // first character in the column, so removing starting whitespace + // could cause a miscount. + let line = if is_fortran { line } else { line.trim() }; + if line.trim().is_empty() { + (1, 0, 0) + } else if is_literate + || comments.iter().any(|c| line.starts_with(c.as_bytes())) + { + (0, 0, 1) + } else { + (0, 1, 0) + } + }) + .reduce(|| (0, 0, 0), |a, b| (a.0 + b.0, a.1 + b.1, a.2 + b.2)) + }; + + let (mut stats, (blanks, code, comments)) = rayon::join(parse_lines, simple_parse); + + stats.blanks += blanks; + stats.code += code; + stats.comments += comments; + stats + } else { + self.parse_lines(config, text, CodeStats::new(), syntax) + } + } + + #[inline] + fn parse_lines( + self, + config: &Config, + lines: &[u8], + mut stats: CodeStats, + mut syntax: SyntaxCounter, + ) -> CodeStats { + let mut stepper = LineStep::new(b'\n', 0, lines.len()); + + while let Some((start, end)) = stepper.next(lines) { + let line = &lines[start..end]; + // FORTRAN has a rule where it only counts as a comment if it's the + // first character in the column, so removing starting whitespace + // could cause a miscount. + let line = if syntax.shared.is_fortran { + line + } else { + line.trim() + }; + trace!("{}", String::from_utf8_lossy(line)); + + if syntax.try_perform_single_line_analysis(line, &mut stats) { + continue; + } + + let started_in_comments = !syntax.stack.is_empty() + || (config.treat_doc_strings_as_comments == Some(true) + && syntax.quote.is_some() + && syntax.quote_is_doc_quote); + let ended_with_comments = + match syntax.perform_multi_line_analysis(lines, start, end, config) { + crate::language::syntax::AnalysisReport::Normal(end) => end, + crate::language::syntax::AnalysisReport::ChildLanguage(FileContext { + language, + end, + stats: blob, + }) => { + match language { + LanguageContext::Markdown { balanced, language } => { + // Add the lines for the code fences. + stats.comments += if balanced { 2 } else { 1 }; + // Add the code inside the fence to the stats. + *stats.blobs.entry(language).or_default() += blob; + } + LanguageContext::Rust => { + // Add all the markdown blobs. + *stats.blobs.entry(LanguageType::Markdown).or_default() += blob; + } + LanguageContext::LinguaFranca => { + let child_lang = syntax.get_lf_target_language(); + *stats.blobs.entry(child_lang).or_default() += blob; + } + LanguageContext::Html { language } => { + stats.code += 1; + // Add all the markdown blobs. + *stats.blobs.entry(language).or_default() += blob; + } + } + + // Advance to after the language code and the delimiter.. + stepper = LineStep::new(b'\n', end, lines.len()); + continue; + } + }; + trace!("{}", String::from_utf8_lossy(line)); + + if syntax.shared.is_literate + || syntax.line_is_comment(line, config, ended_with_comments, started_in_comments) + { + stats.comments += 1; + trace!("Comment No.{}", stats.comments); + trace!("Was the Comment stack empty?: {}", !started_in_comments); + } else { + stats.code += 1; + trace!("Code No.{}", stats.code); + } + } + + stats + } + + fn parse_jupyter(&self, json: &[u8], config: &Config) -> Option { + #[derive(Deserialize)] + struct Jupyter { + cells: Vec, + metadata: JupyterMetadata, + } + + #[derive(Clone, Copy, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "lowercase")] + enum CellType { + Markdown, + Code, + } + + #[derive(Deserialize)] + struct JupyterCell { + cell_type: CellType, + source: Vec, + } + + #[derive(Deserialize)] + struct JupyterMetadata { + kernelspec: serde_json::Value, + language_info: serde_json::Value, + } + + let jupyter: Jupyter = serde_json::from_slice(json).ok()?; + + let mut jupyter_stats = CodeStats::new(); + + let language = jupyter + .metadata + .kernelspec + .get("language") + .and_then(serde_json::Value::as_str) + .and_then(|v| LanguageType::from_str(v).ok()) + .or_else(|| { + jupyter + .metadata + .language_info + .get("file_extension") + .and_then(serde_json::Value::as_str) + .and_then(LanguageType::from_file_extension) + }) + .unwrap_or(LanguageType::Python); + + let iter = jupyter + .cells + .par_iter() + .map(|cell| match cell.cell_type { + CellType::Markdown => ( + LanguageType::Markdown, + LanguageType::Markdown.parse_from_str(cell.source.join(""), config), + ), + CellType::Code => ( + language, + language.parse_from_str(cell.source.join(""), config), + ), + }) + .collect::>(); + + for (language, stats) in iter { + *jupyter_stats.blobs.entry(language).or_default() += &stats; + jupyter_stats += &stats; + } + + Some(jupyter_stats) + } + + /// The embedded language in LF is declared in a construct that looks like this: `target C;`, `target Python`. + /// This is the first thing in the file (although there may be comments before). + fn find_lf_target_language(&self, bytes: &[u8]) -> Option { + use regex::bytes::Regex; + static LF_TARGET_REGEX: Lazy = + Lazy::new(|| Regex::new(r#"(?m)\btarget\s+(\w+)\s*($|;|\{)"#).unwrap()); + LF_TARGET_REGEX.captures(bytes).and_then(|captures| { + let name = captures.get(1).unwrap().as_bytes(); + if name == b"CCpp" { + // this is a special alias for the C target in LF + Some(C) + } else { + let name_str = &String::from_utf8_lossy(name); + let by_name = LanguageType::from_name(name_str); + if by_name.is_none() { + trace!("LF target not recognized: {}", name_str); + } + by_name + } + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::{fs, path::Path}; + + #[test] + fn rust_allows_nested() { + assert!(LanguageType::Rust.allows_nested()); + } + + fn assert_stats(stats: &CodeStats, blanks: usize, code: usize, comments: usize) { + assert_eq!(stats.blanks, blanks, "expected {} blank lines", blanks); + assert_eq!(stats.code, code, "expected {} code lines", code); + assert_eq!( + stats.comments, comments, + "expected {} comment lines", + comments + ); + } + + #[test] + fn jupyter_notebook_has_correct_totals() { + let sample_notebook = + fs::read_to_string(Path::new("tests").join("data").join("jupyter.ipynb")).unwrap(); + + let stats = LanguageType::Jupyter + .parse_jupyter(sample_notebook.as_bytes(), &Config::default()) + .unwrap(); + + assert_stats(&stats, 115, 528, 333); + } + + #[test] + fn lf_embedded_language_is_counted() { + let file_text = + fs::read_to_string(Path::new("tests").join("data").join("linguafranca.lf")).unwrap(); + + let stats = LinguaFranca.parse_from_str(file_text, &Config::default()); + + assert_stats(&stats, 9, 11, 8); + + assert_eq!(stats.blobs.len(), 1, "num embedded languages"); + let rust_stats = stats.blobs.get(&Rust).expect("should have a Rust entry"); + assert_stats(rust_stats, 2, 5, 1); + } +} diff --git a/userland/upstream-src/tokei/src/language/language_type.tera.rs b/userland/upstream-src/tokei/src/language/language_type.tera.rs new file mode 100644 index 0000000000..4dd1be3af4 --- /dev/null +++ b/userland/upstream-src/tokei/src/language/language_type.tera.rs @@ -0,0 +1,470 @@ +use arbitrary::Arbitrary; + +/// Represents a individual programming language. Can be used to provide +/// information about the language, such as multi line comments, single line +/// comments, string literal syntax, whether a given language allows nesting +/// comments. +#[derive(Deserialize)] +#[derive(Arbitrary, Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[non_exhaustive] +#[allow(clippy::upper_case_acronyms)] +pub enum LanguageType { + {% for key, value in languages -%} + #[allow(missing_docs)] {% if value.name is defined %} #[serde(alias = "{{value.name}}")] {% else %} #[serde(alias = "{{key}}")] {% endif %} {{key}}, + {% endfor %} +} + +impl LanguageType { + + /// Returns the display name of a language. + /// + /// ``` + /// # use tokei::*; + /// let bash = LanguageType::Bash; + /// + /// assert_eq!(bash.name(), "BASH"); + /// ``` + pub fn name(self) -> &'static str { + match self { + {% for key, value in languages -%} + {{key}} => {% if value.name %}"{{value.name}}"{% else %}"{{key}}"{% endif %}, + {% endfor %} + } + } + + pub(crate) fn _is_blank(self) -> bool { + match self { + {% for key, v in languages -%} + {{key}} => {{ v.blank | default(value=false) }}, + {% endfor %} + } + } + + pub(crate) fn is_fortran(self) -> bool { + self == LanguageType::FortranModern || + self == LanguageType::FortranLegacy + } + + /// Returns whether the language is "literate", meaning that it considered + /// to primarily be documentation and is counted primarily as comments + /// rather than procedural code. + pub fn is_literate(self) -> bool { + match self { + {% for key, v in languages -%} + {{key}} => {{ v.literate | default(value=false) }}, + {% endfor %} + } + } + + /// Provides every variant in a Vec + pub fn list() -> &'static [(Self, &'static [&'static str])] { + &[{% for key, val in languages -%} + ({{key}}, + {% if val.extensions %} &[{% for extension in val.extensions %}"{{extension}}", {% endfor %}], + {% else %} &[], + {% endif %}), + {% endfor %}] + } + + /// Returns the single line comments of a language. + /// ``` + /// use tokei::LanguageType; + /// let lang = LanguageType::Rust; + /// assert_eq!(lang.line_comments(), &["//"]); + /// ``` + pub fn line_comments(self) -> &'static [&'static str] { + match self { + {% for key, value in languages -%} + {{key}} => &[{% for item in value.line_comment | default(value=[]) %}"{{item}}",{% endfor %}], + {% endfor %} + } + } + + /// Returns the single line comments of a language. + /// ``` + /// use tokei::LanguageType; + /// let lang = LanguageType::Rust; + /// assert_eq!(lang.multi_line_comments(), &[("/*", "*/")]); + /// ``` + pub fn multi_line_comments(self) -> &'static [(&'static str, &'static str)] + { + match self { + {% for key, value in languages -%} + {{key}} => &[ + {%- for items in value.multi_line_comments | default(value=[]) -%} + ({% for item in items %}"{{item}}",{% endfor %}), + {%- endfor -%} + ], + {% endfor %} + } + } + + + /// Returns whether the language allows nested multi line comments. + /// ``` + /// use tokei::LanguageType; + /// let lang = LanguageType::Rust; + /// assert!(lang.allows_nested()); + /// ``` + pub fn allows_nested(self) -> bool { + match self { + {% for key, v in languages -%} + {{key}} => {{ v.nested | default(value=false) }}, + {% endfor %} + } + } + + /// Returns what nested comments the language has. (Currently only D has + /// any of this type.) + /// ``` + /// use tokei::LanguageType; + /// let lang = LanguageType::D; + /// assert_eq!(lang.nested_comments(), &[("/+", "+/")]); + /// ``` + pub fn nested_comments(self) -> &'static [(&'static str, &'static str)] + { + match self { + {% for key, value in languages -%} + {{key}} => &[ + {%- for items in value.nested_comments | default(value=[]) -%} + ({% for item in items %}"{{item}}",{% endfor %}), + {%- endfor -%} + ], + {% endfor %} + } + } + + /// Returns the quotes of a language. + /// ``` + /// use tokei::LanguageType; + /// let lang = LanguageType::C; + /// assert_eq!(lang.quotes(), &[("\"", "\"")]); + /// ``` + pub fn quotes(self) -> &'static [(&'static str, &'static str)] { + match self { + {% for key, value in languages -%} + {{key}} => &[ + {%- for items in value.quotes | default(value=[]) -%} + ({% for item in items %}"{{item}}",{% endfor %}), + {%- endfor -%} + ], + {% endfor %} + } + } + + /// Returns the verbatim quotes of a language. + /// ``` + /// use tokei::LanguageType; + /// let lang = LanguageType::CSharp; + /// assert_eq!(lang.verbatim_quotes(), &[("@\"", "\"")]); + /// ``` + pub fn verbatim_quotes(self) -> &'static [(&'static str, &'static str)] { + match self { + {% for key, value in languages -%} + {{key}} => &[ + {%- for items in value.verbatim_quotes | default(value=[]) -%} + ({% for item in items %}"{{item}}",{% endfor %}), + {%- endfor -%} + ], + {% endfor %} + } + } + + /// Returns the doc quotes of a language. + /// ``` + /// use tokei::LanguageType; + /// let lang = LanguageType::Python; + /// assert_eq!(lang.doc_quotes(), &[("\"\"\"", "\"\"\""), ("'''", "'''")]); + /// ``` + pub fn doc_quotes(self) -> &'static [(&'static str, &'static str)] { + match self { + {% for key, value in languages -%} + {{key}} => &[ + {% for items in value.doc_quotes | default(value=[])-%} + ({% for item in items %}"{{item}}",{% endfor %}), + {%- endfor %} + ], + {%- endfor %} + } + } + + /// Returns the shebang of a language. + /// ``` + /// use tokei::LanguageType; + /// let lang = LanguageType::Bash; + /// assert_eq!(lang.shebangs(), &["#!/bin/bash"]); + /// ``` + pub fn shebangs(self) -> &'static [&'static str] { + match self { + {% for key, lang in languages -%} + {{key}} => &[{% for item in lang.shebangs | default(value=[]) %}"{{item}}",{% endfor %}], + {% endfor %} + } + } + + pub(crate) fn any_multi_line_comments(self) -> &'static [(&'static str, &'static str)] { + match self { + {% for key, value in languages -%} + {{key}} => &[ + {%- set starting_multi_line_comments = value.multi_line_comments | default(value=[]) -%} + {%- set starting_nested_comments = value.nested_comments | default(value=[]) -%} + {%- for item in starting_multi_line_comments | concat(with=starting_nested_comments) -%} + ("{{item.0}}", "{{item.1}}"), + {%- endfor -%} + ], + {% endfor %} + } + } + + pub(crate) fn any_comments(self) -> &'static [&'static str] { + match self { + {% for key, value in languages -%} + {{key}} => &[ + {%- set starting_multi_line_comments = value.multi_line_comments | default(value=[]) -%} + {%- set starting_nested_comments = value.nested_comments | default(value=[]) -%} + + {%- for item in starting_multi_line_comments | concat(with=starting_nested_comments) -%} + "{{item.0}}", + "{{item.1}}", + {%- endfor -%} + {%- for item in value.line_comment | default(value=[]) -%} + "{{item}}", + {%- endfor -%} + ], + {% endfor %} + } + } + + /// Returns the parts of syntax that determines whether tokei can skip large + /// parts of analysis. + pub fn important_syntax(self) -> &'static [&'static str] { + match self { + {% for key, value in languages -%} + {%- set starting_quotes = value.quotes | default(value=[]) | map(attribute="0") -%} + {%- set starting_doc_quotes = value.doc_quotes | default(value=[]) | map(attribute="0") -%} + {%- set starting_multi_line_comments = value.multi_line_comments | default(value=[]) | map(attribute="0") -%} + {%- set starting_nested_comments = value.nested_comments | default(value=[]) | map(attribute="0") -%} + {%- set important_syntax = value.important_syntax | default(value=[]) -%} + + {{key}} => &[ + {%- for item in starting_quotes | + concat(with=starting_doc_quotes) | + concat(with=starting_multi_line_comments) | + concat(with=starting_nested_comments) | + concat(with=important_syntax) -%} + "{{item}}", + {%- endfor -%} + {%- for context in value.contexts | default(value=[]) -%} + {% if value.kind == "html" %} + "<{{context.tag}}", + {% endif %} + {%- endfor -%} + ], + {% endfor %} + } + } + + /// Get language from a file path. May open and read the file. + /// + /// ```no_run + /// use tokei::{Config, LanguageType}; + /// + /// let rust = LanguageType::from_path("./main.rs", &Config::default()); + /// + /// assert_eq!(rust, Some(LanguageType::Rust)); + /// ``` + pub fn from_path>(entry: P, _config: &Config) + -> Option + { + let entry = entry.as_ref(); + + if let Some(filename) = fsutils::get_filename(entry) { + match &*filename { + {% for key, value in languages -%} + {%- if value.filenames -%} + {%- for item in value.filenames -%} + | "{{item}}" + {%- endfor -%} + => return Some({{key}}), + {% endif -%} + {%- endfor %} + _ => () + } + } + + match fsutils::get_extension(entry) { + Some(extension) => LanguageType::from_file_extension(extension.as_str()), + None => LanguageType::from_shebang(entry), + } + } + + /// Get language from a file extension. + /// + /// ```no_run + /// use tokei::LanguageType; + /// + /// let rust = LanguageType::from_file_extension("rs"); + /// + /// assert_eq!(rust, Some(LanguageType::Rust)); + /// ``` + #[must_use] + pub fn from_file_extension(extension: &str) -> Option { + match extension { + {% for key, value in languages -%} + {%- if value.extensions -%} + {%- for item in value.extensions %}| "{{item}}" {% endfor %}=> Some({{key}}), + {% endif -%} + {%- endfor %} + extension => { + warn!("Unknown extension: {}", extension); + None + }, + } + } + + /// Get language from its name. + /// + /// ```no_run + /// use tokei::LanguageType; + /// + /// let rust = LanguageType::from_name("Rust"); + /// + /// assert_eq!(rust, Some(LanguageType::Rust)); + /// ``` + #[must_use] + pub fn from_name(name: &str) -> Option { + match name { + {% for key, value in languages -%} + {% if value.name and value.name != key -%} + | "{{value.name}}" + {% endif -%} + | "{{key}}" => Some({{key}}), + {% endfor %} + unknown => { + warn!("Unknown language name: {}", unknown); + None + }, + } + } + + /// Get language from its MIME type if available. + /// + /// ```no_run + /// use tokei::LanguageType; + /// + /// let lang = LanguageType::from_mime("application/javascript"); + /// + /// assert_eq!(lang, Some(LanguageType::JavaScript)); + /// ``` + #[must_use] + pub fn from_mime(mime: &str) -> Option { + match mime { + {% for key, value in languages -%} + {%- if value.mime -%} + {%- for item in value.mime %}| "{{item}}" {% endfor %}=> Some({{key}}), + {% endif -%} + {%- endfor %} + _ => { + warn!("Unknown MIME: {}", mime); + None + }, + } + } + + /// Get language from a shebang. May open and read the file. + /// + /// ```no_run + /// use tokei::LanguageType; + /// + /// let rust = LanguageType::from_shebang("./main.rs"); + /// + /// assert_eq!(rust, Some(LanguageType::Rust)); + /// ``` + pub fn from_shebang>(entry: P) -> Option { + // Read at max `READ_LIMIT` bytes from the given file. + // A typical shebang line has a length less than 32 characters; + // e.g. '#!/bin/bash' - 11B / `#!/usr/bin/env python3` - 22B + // It is *very* unlikely the file contains a valid shebang syntax + // if we don't find a newline character after searching the first 128B. + const READ_LIMIT: usize = 128; + + let mut file = File::open(entry).ok()?; + let mut buf = [0; READ_LIMIT]; + + let len = file.read(&mut buf).ok()?; + let buf = &buf[..len]; + + let first_line = buf.split(|b| *b == b'\n').next()?; + let first_line = std::str::from_utf8(first_line).ok()?; + + let mut words = first_line.split_whitespace(); + match words.next() { + {# First match against any shebang paths, and then check if the + language matches any found in the environment shebang path. #} + {% for key, value in languages -%} + {%- if value.shebangs %} + {%- for item in value.shebangs %}| Some("{{item}}") {% endfor %}=> Some({{key}}), + {% endif -%} + {%- endfor %} + + Some("#!/usr/bin/env") => { + if let Some(word) = words.next() { + match word { + {% for key, value in languages -%} + {%- if value.env -%} + {%- for item in value.env %} + {% if loop.index == 1 %} + _ if word.starts_with("{{item}}") + {% else %} + || word.starts_with("{{item}}") + {% endif %} + {% endfor %}=> Some({{key}}), + {% endif -%} + {%- endfor %} + env => { + warn!("Unknown environment: {:?}", env); + None + } + } + } else { + None + } + } + _ => None, + } + } +} + +impl FromStr for LanguageType { + type Err = &'static str; + + fn from_str(from: &str) -> Result { + match &*from.to_lowercase() { + {% for key, value in languages %} + {% if value.name %}"{{value.name | lower}}"{% else %}"{{key | lower}}"{% endif %} + => Ok({{key}}), + {% endfor %} + _ => Err("Language not found, please use `-l` to see all available\ + languages."), + } + } +} + +impl fmt::Display for LanguageType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + + +impl<'a> From for Cow<'a, LanguageType> { + fn from(from: LanguageType) -> Self { + Cow::Owned(from) + } +} + +impl<'a> From<&'a LanguageType> for Cow<'a, LanguageType> { + fn from(from: &'a LanguageType) -> Self { + Cow::Borrowed(from) + } +} diff --git a/userland/upstream-src/tokei/src/language/languages.rs b/userland/upstream-src/tokei/src/language/languages.rs new file mode 100644 index 0000000000..87a749ac7f --- /dev/null +++ b/userland/upstream-src/tokei/src/language/languages.rs @@ -0,0 +1,166 @@ +use std::{ + collections::{btree_map, BTreeMap}, + iter::IntoIterator, + ops::{AddAssign, Deref, DerefMut}, + path::Path, +}; + +use rayon::prelude::*; + +use crate::{ + config::Config, + language::{Language, LanguageType}, + utils, +}; + +/// A newtype representing a list of languages counted in the provided +/// directory. +/// ([_List of +/// Languages_](https://github.com/XAMPPRocky/tokei#supported-languages)) +#[derive(Debug, Default, PartialEq)] +pub struct Languages { + inner: BTreeMap, +} + +impl serde::Serialize for Languages { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.inner.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Languages { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map = <_>::deserialize(deserializer)?; + + Ok(Self::from_previous(map)) + } +} + +impl Languages { + fn from_previous(map: BTreeMap) -> Self { + use std::collections::btree_map::Entry; + let mut me = Self::new(); + + for (name, input_language) in map { + match me.entry(name) { + Entry::Occupied(mut entry) => { + *entry.get_mut() += input_language; + } + Entry::Vacant(entry) => { + entry.insert(input_language); + } + } + } + me + } + + /// Populates the `Languages` struct with statistics about languages + /// provided by [`Language`]. + /// + /// Takes a `&[&str]` of paths to recursively traverse, paths can be + /// relative, absolute or glob paths. A second `&[&str]` of paths to ignore, + /// these strings use the `.gitignore` syntax, such as `target` + /// or `**/*.bk`. + /// + /// ```no_run + /// use tokei::{Config, Languages}; + /// + /// let mut languages = Languages::new(); + /// languages.get_statistics(&["."], &[".git", "target"], &Config::default()); + /// ``` + /// + /// [`Language`]: struct.Language.html + pub fn get_statistics>( + &mut self, + paths: &[A], + ignored: &[&str], + config: &Config, + ) { + utils::fs::get_all_files(paths, ignored, &mut self.inner, config); + self.inner.par_iter_mut().for_each(|(_, l)| l.total()); + } + + /// Constructs a new, Languages struct. Languages is always empty and does + /// not allocate. + /// + /// ```rust + /// # use tokei::*; + /// let languages = Languages::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Languages::default() + } + + /// Summary of the Languages struct. + #[must_use] + pub fn total(self: &Languages) -> Language { + let mut total = Language::new(); + for (ty, l) in self { + let language = l.summarise(); + total.comments += language.comments; + total.blanks += language.blanks; + total.code += language.code; + total.inaccurate |= language.inaccurate; + total.children.insert(*ty, language.reports.clone()); + } + total + } +} + +impl IntoIterator for Languages { + type Item = as IntoIterator>::Item; + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} + +impl<'a> IntoIterator for &'a Languages { + type Item = (&'a LanguageType, &'a Language); + type IntoIter = btree_map::Iter<'a, LanguageType, Language>; + + fn into_iter(self) -> Self::IntoIter { + self.inner.iter() + } +} + +impl<'a> IntoIterator for &'a mut Languages { + type Item = (&'a LanguageType, &'a mut Language); + type IntoIter = btree_map::IterMut<'a, LanguageType, Language>; + + fn into_iter(self) -> Self::IntoIter { + self.inner.iter_mut() + } +} + +impl AddAssign> for Languages { + fn add_assign(&mut self, rhs: BTreeMap) { + for (name, language) in rhs { + if let Some(result) = self.inner.get_mut(&name) { + *result += language; + } + } + } +} + +impl Deref for Languages { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Languages { + fn deref_mut(&mut self) -> &mut BTreeMap { + &mut self.inner + } +} diff --git a/userland/upstream-src/tokei/src/language/mod.rs b/userland/upstream-src/tokei/src/language/mod.rs new file mode 100644 index 0000000000..f6ffa06cab --- /dev/null +++ b/userland/upstream-src/tokei/src/language/mod.rs @@ -0,0 +1,176 @@ +mod embedding; +pub mod language_type; +pub mod languages; +mod syntax; + +use std::{collections::BTreeMap, mem, ops::AddAssign}; + +pub use self::{language_type::*, languages::Languages}; + +use crate::{sort::Sort, stats::Report}; + +/// A struct representing statistics about a single Language. +#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize)] +pub struct Language { + /// The total number of blank lines. + pub blanks: usize, + /// The total number of lines of code. + pub code: usize, + /// The total number of comments(both single, and multi-line) + pub comments: usize, + /// A collection of statistics of individual files. + pub reports: Vec, + /// A map of any languages found in the reports. + pub children: BTreeMap>, + /// Whether this language had problems with file parsing + pub inaccurate: bool, +} + +impl Language { + /// Constructs a new empty Language with the comments provided. + /// + /// ``` + /// # use tokei::*; + /// let mut rust = Language::new(); + /// ``` + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Returns the total number of lines. + #[inline] + #[must_use] + pub fn lines(&self) -> usize { + self.blanks + self.code + self.comments + } + + /// Add a `Report` to the Language. This will not update the totals in the + /// Language struct. + pub fn add_report(&mut self, report: Report) { + for (lang, stats) in &report.stats.blobs { + let mut new_report = Report::new(report.name.clone()); + new_report.stats = stats.clone(); + + self.children.entry(*lang).or_default().push(new_report); + } + + self.reports.push(report); + } + + /// Marks this language as possibly not reflecting correct stats. + #[inline] + pub fn mark_inaccurate(&mut self) { + self.inaccurate = true; + } + + /// Creates a new `Language` from `self`, which is a summarised version + /// of the language that doesn't contain any children. It will count + /// non-blank lines in child languages as code unless the child language is + /// considered "literate" then it will be counted as comments. + #[must_use] + pub fn summarise(&self) -> Language { + let mut summary = self.clone(); + + for reports in self.children.values() { + for stats in reports.iter().map(|r| r.stats.summarise()) { + summary.comments += stats.comments; + summary.code += stats.code; + summary.blanks += stats.blanks; + } + } + + summary + } + + /// Totals up the statistics of the `Stat` structs currently contained in + /// the language. + /// + /// ```no_run + /// use std::{collections::BTreeMap, path::PathBuf}; + /// use tokei::Language; + /// + /// let mut language = Language::new(); + /// + /// // Add stats... + /// + /// assert_eq!(0, language.lines()); + /// + /// language.total(); + /// + /// assert_eq!(10, language.lines()); + /// ``` + pub fn total(&mut self) { + let mut blanks = 0; + let mut code = 0; + let mut comments = 0; + + for report in &self.reports { + blanks += report.stats.blanks; + code += report.stats.code; + comments += report.stats.comments; + } + + self.blanks = blanks; + self.code = code; + self.comments = comments; + } + + /// Checks if the language is empty. Empty meaning it doesn't have any + /// statistics. + /// + /// ``` + /// # use tokei::*; + /// let rust = Language::new(); + /// + /// assert!(rust.is_empty()); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.code == 0 && self.comments == 0 && self.blanks == 0 && self.children.is_empty() + } + + /// Sorts each of the `Report`s contained in the language based + /// on what category is provided. + /// + /// ```no_run + /// use std::{collections::BTreeMap, path::PathBuf}; + /// use tokei::{Language, Sort}; + /// + /// let mut language = Language::new(); + /// + /// // Add stats... + /// + /// language.sort_by(Sort::Lines); + /// assert_eq!(20, language.reports[0].stats.lines()); + /// + /// language.sort_by(Sort::Code); + /// assert_eq!(8, language.reports[0].stats.code); + /// ``` + pub fn sort_by(&mut self, category: Sort) { + match category { + Sort::Blanks => self + .reports + .sort_by(|a, b| b.stats.blanks.cmp(&a.stats.blanks)), + Sort::Comments => self + .reports + .sort_by(|a, b| b.stats.comments.cmp(&a.stats.comments)), + Sort::Code => self.reports.sort_by(|a, b| b.stats.code.cmp(&a.stats.code)), + Sort::Files => self.reports.sort_by(|a, b| a.name.cmp(&b.name)), + Sort::Lines => self + .reports + .sort_by(|a, b| b.stats.lines().cmp(&a.stats.lines())), + } + } +} + +impl AddAssign for Language { + fn add_assign(&mut self, mut rhs: Self) { + self.comments += rhs.comments; + self.blanks += rhs.blanks; + self.code += rhs.code; + self.reports.extend(mem::take(&mut rhs.reports)); + self.children.extend(mem::take(&mut rhs.children)); + self.inaccurate |= rhs.inaccurate; + } +} diff --git a/userland/upstream-src/tokei/src/language/syntax.rs b/userland/upstream-src/tokei/src/language/syntax.rs new file mode 100644 index 0000000000..95a3186ea2 --- /dev/null +++ b/userland/upstream-src/tokei/src/language/syntax.rs @@ -0,0 +1,683 @@ +use std::sync::Arc; + +use aho_corasick::AhoCorasick; +use dashmap::DashMap; +use grep_searcher::LineStep; +use log::Level::Trace; +use once_cell::sync::Lazy; + +use super::embedding::{ + RegexCache, RegexFamily, ENDING_LF_BLOCK_REGEX, ENDING_MARKDOWN_REGEX, END_SCRIPT, END_STYLE, + END_TEMPLATE, +}; +use crate::LanguageType::LinguaFranca; +use crate::{stats::CodeStats, utils::ext::SliceExt, Config, LanguageType}; + +/// Tracks the syntax of the language as well as the current state in the file. +/// Current has what could be consider three types of mode. +/// - `plain` mode: This is the normal state, blanks are counted as blanks, +/// string literals can trigger `string` mode, and comments can trigger +/// `comment` mode. +/// - `string` mode: This when the state machine is current inside a string +/// literal for a given language, comments cannot trigger `comment` mode while +/// in `string` mode. +/// - `comment` mode: This when the state machine is current inside a comment +/// for a given language, strings cannot trigger `string` mode while in +/// `comment` mode. +#[derive(Clone, Debug)] +pub(crate) struct SyntaxCounter { + pub(crate) shared: Arc, + pub(crate) quote: Option<&'static str>, + pub(crate) quote_is_doc_quote: bool, + pub(crate) stack: Vec<&'static str>, + pub(crate) quote_is_verbatim: bool, + pub(crate) lf_embedded_language: Option, +} + +#[derive(Clone, Debug)] +pub(crate) struct FileContext { + pub(crate) language: LanguageContext, + pub(crate) stats: CodeStats, + pub(crate) end: usize, +} + +impl FileContext { + pub fn new(language: LanguageContext, end: usize, stats: CodeStats) -> Self { + Self { + language, + stats, + end, + } + } +} + +#[derive(Clone, Debug)] +pub(crate) enum LanguageContext { + Html { + language: LanguageType, + }, + LinguaFranca, + Markdown { + balanced: bool, + language: LanguageType, + }, + Rust, +} + +#[derive(Clone, Debug)] +pub(crate) struct SharedMatchers { + pub language: LanguageType, + pub allows_nested: bool, + pub doc_quotes: &'static [(&'static str, &'static str)], + pub important_syntax: AhoCorasick, + #[allow(dead_code)] + pub any_comments: &'static [&'static str], + pub is_fortran: bool, + pub is_literate: bool, + pub line_comments: &'static [&'static str], + pub any_multi_line_comments: &'static [(&'static str, &'static str)], + pub multi_line_comments: &'static [(&'static str, &'static str)], + pub nested_comments: &'static [(&'static str, &'static str)], + pub string_literals: &'static [(&'static str, &'static str)], + pub verbatim_string_literals: &'static [(&'static str, &'static str)], +} + +impl SharedMatchers { + pub fn new(language: LanguageType) -> Arc { + static MATCHERS: Lazy>> = Lazy::new(DashMap::new); + + MATCHERS + .entry(language) + .or_insert_with(|| Arc::new(Self::init(language))) + .value() + .clone() + } + + pub fn init(language: LanguageType) -> Self { + fn init_corasick(pattern: &[&'static str]) -> AhoCorasick { + AhoCorasick::builder() + .match_kind(aho_corasick::MatchKind::LeftmostLongest) + .start_kind(aho_corasick::StartKind::Unanchored) + .prefilter(true) + .kind(Some(aho_corasick::AhoCorasickKind::DFA)) + .build(pattern) + .unwrap() + } + + Self { + language, + allows_nested: language.allows_nested(), + doc_quotes: language.doc_quotes(), + is_fortran: language.is_fortran(), + is_literate: language.is_literate(), + important_syntax: init_corasick(language.important_syntax()), + any_comments: language.any_comments(), + line_comments: language.line_comments(), + multi_line_comments: language.multi_line_comments(), + any_multi_line_comments: language.any_multi_line_comments(), + nested_comments: language.nested_comments(), + string_literals: language.quotes(), + verbatim_string_literals: language.verbatim_quotes(), + } + } +} + +#[derive(Debug)] +pub(crate) enum AnalysisReport { + /// No child languages were found, contains a boolean representing whether + /// the line ended with comments or not. + Normal(bool), + ChildLanguage(FileContext), +} + +impl SyntaxCounter { + pub(crate) fn new(language: LanguageType) -> Self { + Self { + shared: SharedMatchers::new(language), + quote_is_doc_quote: false, + quote_is_verbatim: false, + stack: Vec::with_capacity(1), + lf_embedded_language: None, + quote: None, + } + } + + /// Returns whether the syntax is currently in plain mode. + pub(crate) fn is_plain_mode(&self) -> bool { + self.quote.is_none() && self.stack.is_empty() + } + + /// Returns whether the syntax is currently in string mode. + pub(crate) fn _is_string_mode(&self) -> bool { + self.quote.is_some() + } + + /// Returns whether the syntax is currently in comment mode. + pub(crate) fn _is_comment_mode(&self) -> bool { + !self.stack.is_empty() + } + + pub(crate) fn get_lf_target_language(&self) -> LanguageType { + // in case the target declaration was not found, default it to that language + const DEFAULT_LANG: LanguageType = LinguaFranca; + self.lf_embedded_language.unwrap_or(DEFAULT_LANG) + } + + #[inline] + pub(crate) fn parse_line_comment(&self, window: &[u8]) -> bool { + if self.quote.is_some() || !self.stack.is_empty() { + false + } else if let Some(comment) = self + .shared + .line_comments + .iter() + .find(|c| window.starts_with(c.as_bytes())) + { + trace!("Start {:?}", comment); + true + } else { + false + } + } + + /// Try to see if we can determine what a line is from examining the whole + /// line at once. Returns `true` if successful. + pub(crate) fn try_perform_single_line_analysis( + &self, + line: &[u8], + stats: &mut crate::stats::CodeStats, + ) -> bool { + if !self.is_plain_mode() { + false + } else if line.trim().is_empty() { + stats.blanks += 1; + trace!("Blank No.{}", stats.blanks); + true + } else if self.shared.important_syntax.is_match(line) { + false + } else { + trace!("^ Skippable"); + + if self.shared.is_literate + || self + .shared + .line_comments + .iter() + .any(|c| line.starts_with(c.as_bytes())) + { + stats.comments += 1; + trace!("Comment No.{}", stats.comments); + } else { + stats.code += 1; + trace!("Code No.{}", stats.code); + } + + true + } + } + + pub(crate) fn perform_multi_line_analysis( + &mut self, + lines: &[u8], + start: usize, + end: usize, + config: &Config, + ) -> AnalysisReport { + let mut ended_with_comments = false; + let mut skip = 0; + macro_rules! skip { + ($skip:expr) => {{ + skip = $skip - 1; + }}; + } + + let regex_cache = RegexCache::build(self.shared.language, lines, start, end); + + for i in start..end { + if skip != 0 { + skip -= 1; + continue; + } + + let window = &lines[i..]; + + if window.trim().is_empty() { + break; + } + + ended_with_comments = false; + let is_end_of_quote_or_multi_line = self + .parse_end_of_quote(window) + .or_else(|| self.parse_end_of_multi_line(window)); + + if let Some(skip_amount) = is_end_of_quote_or_multi_line { + ended_with_comments = true; + skip!(skip_amount); + continue; + } else if self.quote.is_some() { + continue; + } + + if let Some(child) = self.parse_context(lines, i, end, config, ®ex_cache) { + return AnalysisReport::ChildLanguage(child); + } + + let is_quote_or_multi_line = self + .parse_quote(window) + .or_else(|| self.parse_multi_line_comment(window)); + + if let Some(skip_amount) = is_quote_or_multi_line { + skip!(skip_amount); + continue; + } + + if self.parse_line_comment(window) { + ended_with_comments = true; + break; + } + } + + AnalysisReport::Normal(ended_with_comments) + } + + /// Performs a set of heuristics to determine whether a line is a comment or + /// not. The procedure is as follows. + /// + /// - Yes/No: Counted as Comment + /// + /// 1. Check if we're in string mode + /// 1. Check if string literal is a doc string and whether tokei has + /// been configured to treat them as comments. + /// - Yes: When the line starts with the doc string or when we are + /// continuing from a previous line. + /// - No: The string is a normal string literal or tokei isn't + /// configured to count them as comments. + /// 2. If we're not in string mode, check if we left it this on this line. + /// - Yes: When we found a doc quote and we started in comments. + /// 3. Yes: When the whole line is a comment e.g. `/* hello */` + /// 4. Yes: When the previous line started a multi-line comment. + /// 5. Yes: When the line starts with a comment. + /// 6. No: Any other input. + pub(crate) fn line_is_comment( + &self, + line: &[u8], + config: &crate::Config, + _ended_with_comments: bool, + started_in_comments: bool, + ) -> bool { + let trimmed = line.trim(); + let whole_line_is_comment = || { + self.shared + .line_comments + .iter() + .any(|c| trimmed.starts_with(c.as_bytes())) + || self + .shared + .any_multi_line_comments + .iter() + .any(|(start, end)| { + trimmed.starts_with(start.as_bytes()) && trimmed.ends_with(end.as_bytes()) + }) + }; + let starts_with_comment = || { + let quote = match self.stack.last() { + Some(q) => q, + _ => return false, + }; + + self.shared + .any_multi_line_comments + .iter() + .any(|(start, end)| end == quote && trimmed.starts_with(start.as_bytes())) + }; + + // `Some(true)` in order to respect the current configuration. + #[allow(clippy::if_same_then_else)] + if self.quote.is_some() { + if self.quote_is_doc_quote && config.treat_doc_strings_as_comments == Some(true) { + self.quote.map_or(false, |q| line.starts_with(q.as_bytes())) + || (self.quote.is_some()) + } else { + false + } + } else if self + .shared + .doc_quotes + .iter() + .any(|(_, e)| line.contains_slice(e.as_bytes())) + && started_in_comments + { + true + } else if (whole_line_is_comment)() { + true + } else if started_in_comments { + true + } else { + (starts_with_comment)() + } + } + + #[inline] + pub(crate) fn parse_context( + &mut self, + lines: &[u8], + start: usize, + end: usize, + config: &Config, + regex_cache: &RegexCache, + ) -> Option { + use std::str::FromStr; + + // static TYPE_REGEX: Lazy = Lazy::new(|| Regex::new(r#"type="(.*)".*>"#).unwrap()); + if self.quote.is_some() || !self.stack.is_empty() { + return None; + } + + match regex_cache.family()? { + RegexFamily::Markdown(md) => { + if !lines[start..end].contains_slice(b"```") { + return None; + } + + let opening_fence = md.starts_in_range(start, end)?; + let start_of_code = opening_fence.end(); + let closing_fence = ENDING_MARKDOWN_REGEX.find(&lines[start_of_code..]); + if let Some(m) = &closing_fence { + trace!("{:?}", String::from_utf8_lossy(m.as_bytes())); + } + let end_of_code = closing_fence + .map_or_else(|| lines.len(), |fence| start_of_code + fence.start()); + let end_of_code_block = + closing_fence.map_or_else(|| lines.len(), |fence| start_of_code + fence.end()); + let balanced = closing_fence.is_some(); + let identifier = &opening_fence.as_bytes().trim()[3..]; + + let language = identifier + .split(|&b| b == b',') + .find_map(|s| LanguageType::from_str(&String::from_utf8_lossy(s)).ok())?; + trace!( + "{} BLOCK: {:?}", + language, + String::from_utf8_lossy(&lines[start_of_code..end_of_code]) + ); + let stats = + language.parse_from_slice(lines[start_of_code..end_of_code].trim(), config); + + Some(FileContext::new( + LanguageContext::Markdown { balanced, language }, + end_of_code_block, + stats, + )) + } + RegexFamily::Rust => { + let rest = &lines[start..]; + let comment_syntax = if rest.trim_start().starts_with(b"///") { + b"///" + } else if rest.trim_start().starts_with(b"//!") { + b"//!" + } else { + return None; + }; + + let mut stepper = LineStep::new(b'\n', start, lines.len()); + let mut markdown = Vec::new(); + let mut end_of_block = lines.len(); + + while let Some((start, end)) = stepper.next(lines) { + if lines[start..].trim().starts_with(comment_syntax) { + trace!("{}", String::from_utf8_lossy(&lines[start..end])); + let line = lines[start..end].trim_start(); + let stripped_line = &line[3.min(line.len())..]; + markdown.extend_from_slice(stripped_line); + end_of_block = end; + } else { + end_of_block = start; + break; + } + } + + trace!("Markdown found: {:?}", String::from_utf8_lossy(&markdown)); + let doc_block = LanguageType::Markdown.parse_from_slice(markdown.trim(), config); + + Some(FileContext::new( + LanguageContext::Rust, + end_of_block, + doc_block, + )) + } + RegexFamily::LinguaFranca(lf) => { + let opening_fence = lf.starts_in_range(start, end)?; + let start_of_code = opening_fence.end(); + let closing_fence = ENDING_LF_BLOCK_REGEX.find(&lines[start_of_code..]); + let end_of_code = closing_fence + .map_or_else(|| lines.len(), |fence| start_of_code + fence.start()); + + let block_contents = &lines[start_of_code..end_of_code]; + trace!("LF block: {:?}", String::from_utf8_lossy(block_contents)); + let stats = self.get_lf_target_language().parse_from_slice( + block_contents.trim_first_and_last_line_of_whitespace(), + config, + ); + trace!("-> stats: {:?}", stats); + + Some(FileContext::new( + LanguageContext::LinguaFranca, + end_of_code, + stats, + )) + } + RegexFamily::HtmlLike(html) => { + if let Some(mut captures) = html.start_script_in_range(start, end) { + let start_of_code = captures.next().unwrap().end(); + let closing_tag = END_SCRIPT.find(&lines[start_of_code..])?; + let end_of_code = start_of_code + closing_tag.start(); + let language = captures + .next() + .and_then(|m| { + LanguageType::from_mime(&String::from_utf8_lossy(m.as_bytes().trim())) + }) + .unwrap_or(LanguageType::JavaScript); + let script_contents = &lines[start_of_code..end_of_code]; + if script_contents.trim().is_empty() { + return None; + } + + let stats = language.parse_from_slice( + script_contents.trim_first_and_last_line_of_whitespace(), + config, + ); + Some(FileContext::new( + LanguageContext::Html { language }, + end_of_code, + stats, + )) + } else if let Some(mut captures) = html.start_style_in_range(start, end) { + let start_of_code = captures.next().unwrap().end(); + let closing_tag = END_STYLE.find(&lines[start_of_code..])?; + let end_of_code = start_of_code + closing_tag.start(); + let language = captures + .next() + .and_then(|m| { + LanguageType::from_str( + &String::from_utf8_lossy(m.as_bytes().trim()).to_lowercase(), + ) + .ok() + }) + .unwrap_or(LanguageType::Css); + let style_contents = &lines[start_of_code..end_of_code]; + if style_contents.trim().is_empty() { + return None; + } + + let stats = language.parse_from_slice( + style_contents.trim_first_and_last_line_of_whitespace(), + config, + ); + Some(FileContext::new( + LanguageContext::Html { language }, + end_of_code, + stats, + )) + } else if let Some(mut captures) = html.start_template_in_range(start, end) { + let start_of_code = captures.next().unwrap().end(); + let closing_tag = END_TEMPLATE.find(&lines[start_of_code..])?; + let end_of_code = start_of_code + closing_tag.start(); + let language = captures + .next() + .and_then(|m| { + LanguageType::from_str( + &String::from_utf8_lossy(m.as_bytes().trim()).to_lowercase(), + ) + .ok() + }) + .unwrap_or(LanguageType::Html); + + let template_contents = &lines[start_of_code..end_of_code]; + if template_contents.trim().is_empty() { + return None; + } + let stats = language.parse_from_slice( + template_contents.trim_first_and_last_line_of_whitespace(), + config, + ); + Some(FileContext::new( + LanguageContext::Html { language }, + end_of_code, + stats, + )) + } else { + None + } + } + } + } + + #[inline] + pub(crate) fn parse_quote(&mut self, window: &[u8]) -> Option { + if !self.stack.is_empty() { + return None; + } + + if let Some((start, end)) = self + .shared + .doc_quotes + .iter() + .find(|(s, _)| window.starts_with(s.as_bytes())) + { + trace!("Start Doc {:?}", start); + self.quote = Some(end); + self.quote_is_verbatim = false; + self.quote_is_doc_quote = true; + return Some(start.len()); + } + + if let Some((start, end)) = self + .shared + .verbatim_string_literals + .iter() + .find(|(s, _)| window.starts_with(s.as_bytes())) + { + trace!("Start verbatim {:?}", start); + self.quote = Some(end); + self.quote_is_verbatim = true; + self.quote_is_doc_quote = false; + return Some(start.len()); + } + + if let Some((start, end)) = self + .shared + .string_literals + .iter() + .find(|(s, _)| window.starts_with(s.as_bytes())) + { + trace!("Start {:?}", start); + self.quote = Some(end); + self.quote_is_verbatim = false; + self.quote_is_doc_quote = false; + return Some(start.len()); + } + + None + } + + #[inline] + pub(crate) fn parse_end_of_quote(&mut self, window: &[u8]) -> Option { + #[allow(clippy::if_same_then_else)] + if self._is_string_mode() && window.starts_with(self.quote?.as_bytes()) { + let quote = self.quote.take().unwrap(); + trace!("End {:?}", quote); + Some(quote.len()) + } else if !self.quote_is_verbatim && window.starts_with(br"\\") { + Some(2) + } else if !self.quote_is_verbatim + && window.starts_with(br"\") + && self + .shared + .string_literals + .iter() + .any(|(start, _)| window[1..].starts_with(start.as_bytes())) + { + // Tell the state machine to skip the next character because it + // has been escaped if the string isn't a verbatim string. + Some(2) + } else { + None + } + } + + #[inline] + pub(crate) fn parse_multi_line_comment(&mut self, window: &[u8]) -> Option { + if self.quote.is_some() { + return None; + } + + let iter = self + .shared + .multi_line_comments + .iter() + .chain(self.shared.nested_comments); + for &(start, end) in iter { + if window.starts_with(start.as_bytes()) { + if self.stack.is_empty() + || self.shared.allows_nested + || self.shared.nested_comments.contains(&(start, end)) + { + self.stack.push(end); + + if log_enabled!(Trace) && self.shared.allows_nested { + trace!("Start nested {:?}", start); + } else { + trace!("Start {:?}", start); + } + } + + return Some(start.len()); + } + } + + None + } + + #[inline] + pub(crate) fn parse_end_of_multi_line(&mut self, window: &[u8]) -> Option { + if self + .stack + .last() + .map_or(false, |l| window.starts_with(l.as_bytes())) + { + let last = self.stack.pop().unwrap(); + + if log_enabled!(Trace) { + if self.stack.is_empty() { + trace!("End {:?}", last); + } else { + trace!("End {:?}. Still in comments.", last); + } + } + + Some(last.len()) + } else { + None + } + } +} diff --git a/userland/upstream-src/tokei/src/lib.rs b/userland/upstream-src/tokei/src/lib.rs new file mode 100644 index 0000000000..a2b4d0e662 --- /dev/null +++ b/userland/upstream-src/tokei/src/lib.rs @@ -0,0 +1,64 @@ +//! # Tokei: Count your code quickly. +//! +//! A simple, efficient library for counting code in directories. This +//! functionality is also provided as a +//! [CLI utility](//github.com/XAMPPRocky/tokei). Tokei uses a small state +//! machine rather than regular expressions found in other code counters. Tokei +//! can accurately count a lot more edge cases such as nested comments, or +//! comment syntax inside string literals. +//! +//! # Examples +//! +//! Gets the total lines of code from all rust files in current directory, +//! and all subdirectories. +//! +//! ```no_run +//! use std::collections::BTreeMap; +//! use std::fs::File; +//! use std::io::Read; +//! +//! use tokei::{Config, Languages, LanguageType}; +//! +//! // The paths to search. Accepts absolute, relative, and glob paths. +//! let paths = &["src", "tests"]; +//! // Exclude any path that contains any of these strings. +//! let excluded = &["target"]; +//! // `Config` allows you to configure what is searched and counted. +//! let config = Config::default(); +//! +//! let mut languages = Languages::new(); +//! languages.get_statistics(paths, excluded, &config); +//! let rust = &languages[&LanguageType::Rust]; +//! +//! println!("Lines of code: {}", rust.code); +//! ``` + +#![deny( + trivial_casts, + trivial_numeric_casts, + unused_variables, + unstable_features, + unused_import_braces, + missing_docs +)] + +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde; + +#[macro_use] +mod utils; +mod config; +mod consts; +mod language; +mod sort; +mod stats; + +pub use self::{ + config::Config, + consts::*, + language::{Language, LanguageType, Languages}, + sort::Sort, + stats::{find_char_boundary, CodeStats, Report}, +}; diff --git a/userland/upstream-src/tokei/src/main.rs b/userland/upstream-src/tokei/src/main.rs new file mode 100644 index 0000000000..62619dfe69 --- /dev/null +++ b/userland/upstream-src/tokei/src/main.rs @@ -0,0 +1,128 @@ +#[macro_use] +extern crate log; + +mod cli; +mod cli_utils; +mod consts; +mod input; + +use std::{error::Error, io, process}; + +use tokei::{Config, Languages, Sort}; + +use crate::{ + cli::Cli, + cli_utils::Printer, + consts::{ + BLANKS_COLUMN_WIDTH, CODE_COLUMN_WIDTH, COMMENTS_COLUMN_WIDTH, FALLBACK_ROW_LEN, + LANGUAGE_COLUMN_WIDTH, LINES_COLUMN_WIDTH, PATH_COLUMN_WIDTH, + }, + input::add_input, +}; + +fn main() -> Result<(), Box> { + let mut cli = Cli::from_args(); + + if cli.print_languages { + Cli::print_supported_languages()?; + process::exit(0); + } + let config = cli.override_config(Config::from_config_files()); + let mut languages = Languages::new(); + + if let Some(input) = cli.file_input() { + if !add_input(input, &mut languages) { + Cli::print_input_parse_failure(input); + process::exit(1); + } + } + + let input = cli.input(); + + for path in &input { + if ::std::fs::metadata(path).is_err() { + eprintln!("Error: '{}' not found.", path); + process::exit(1); + } + } + + let columns = cli + .columns + .or(config.columns) + .or_else(|| { + if cli.files { + term_size::dimensions().map(|(w, _)| w) + } else { + None + } + }) + .unwrap_or(FALLBACK_ROW_LEN) + .max(FALLBACK_ROW_LEN); + + if cli.streaming == Some(crate::cli::Streaming::Simple) { + println!( + "#{:^LANGUAGE_COLUMN_WIDTH$} {:^PATH_COLUMN_WIDTH$} {:^LINES_COLUMN_WIDTH$} {:^CODE_COLUMN_WIDTH$} {:^COMMENTS_COLUMN_WIDTH$} {:^BLANKS_COLUMN_WIDTH$}", + "language", "path", "lines", "code", "comments", "blanks" + ); + println!( + "{:>LANGUAGE_COLUMN_WIDTH$} {:LINES_COLUMN_WIDTH$} {:>CODE_COLUMN_WIDTH$} {:>COMMENTS_COLUMN_WIDTH$} {:>BLANKS_COLUMN_WIDTH$}", + (0..10).map(|_| "#").collect::(), + (0..80).map(|_| "#").collect::(), + (0..12).map(|_| "#").collect::(), + (0..12).map(|_| "#").collect::(), + (0..12).map(|_| "#").collect::(), + (0..12).map(|_| "#").collect::() + ); + } + + languages.get_statistics(&input, &cli.ignored_directories(), &config); + if config.for_each_fn.is_some() { + process::exit(0); + } + + if let Some(format) = cli.output { + print!("{}", format.print(&languages).unwrap()); + process::exit(0); + } + + let mut printer = Printer::new( + columns, + cli.files, + io::BufWriter::new(io::stdout()), + cli.number_format, + ); + + if languages.iter().any(|(_, lang)| lang.inaccurate) { + printer.print_inaccuracy_warning()?; + } + + printer.print_header()?; + + let mut is_sorted = false; + if let Some(sort_category) = cli.sort.or(config.sort) { + for (_, ref mut language) in &mut languages { + language.sort_by(sort_category); + } + + let mut languages: Vec<_> = languages.iter().collect(); + match sort_category { + Sort::Blanks => languages.sort_by(|a, b| b.1.blanks.cmp(&a.1.blanks)), + Sort::Comments => languages.sort_by(|a, b| b.1.comments.cmp(&a.1.comments)), + Sort::Code => languages.sort_by(|a, b| b.1.code.cmp(&a.1.code)), + Sort::Files => languages.sort_by(|a, b| b.1.reports.len().cmp(&a.1.reports.len())), + Sort::Lines => languages.sort_by(|a, b| b.1.lines().cmp(&a.1.lines())), + } + is_sorted = true; + if cli.sort_reverse { + printer.print_results(languages.into_iter().rev(), cli.compact, is_sorted)?; + } else { + printer.print_results(languages.into_iter(), cli.compact, is_sorted)?; + } + } else { + printer.print_results(languages.iter(), cli.compact, is_sorted)?; + } + + printer.print_total(&languages)?; + + Ok(()) +} diff --git a/userland/upstream-src/tokei/src/sort.rs b/userland/upstream-src/tokei/src/sort.rs new file mode 100644 index 0000000000..4673b01890 --- /dev/null +++ b/userland/upstream-src/tokei/src/sort.rs @@ -0,0 +1,61 @@ +use std::{borrow::Cow, str::FromStr}; + +use serde::de::{self, Deserialize, Deserializer}; + +/// Used for sorting languages. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum Sort { + /// Sort by number blank lines. + Blanks, + /// Sort by number comments lines. + Comments, + /// Sort by number code lines. + Code, + /// Sort by number files lines. + Files, + /// Sort by number of lines. + Lines, +} + +impl FromStr for Sort { + type Err = String; + + fn from_str(s: &str) -> Result { + Ok(if s.eq_ignore_ascii_case("blanks") { + Sort::Blanks + } else if s.eq_ignore_ascii_case("comments") { + Sort::Comments + } else if s.eq_ignore_ascii_case("code") { + Sort::Code + } else if s.eq_ignore_ascii_case("files") { + Sort::Files + } else if s.eq_ignore_ascii_case("lines") { + Sort::Lines + } else { + return Err(format!("Unsupported sorting option: {}", s)); + }) + } +} + +impl<'de> Deserialize<'de> for Sort { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(de::Error::custom) + } +} + +impl<'a> From for Cow<'a, Sort> { + fn from(from: Sort) -> Self { + Cow::Owned(from) + } +} + +impl<'a> From<&'a Sort> for Cow<'a, Sort> { + fn from(from: &'a Sort) -> Self { + Cow::Borrowed(from) + } +} diff --git a/userland/upstream-src/tokei/src/stats.rs b/userland/upstream-src/tokei/src/stats.rs new file mode 100644 index 0000000000..5562e65e77 --- /dev/null +++ b/userland/upstream-src/tokei/src/stats.rs @@ -0,0 +1,143 @@ +use crate::consts::{ + BLANKS_COLUMN_WIDTH, CODE_COLUMN_WIDTH, COMMENTS_COLUMN_WIDTH, LINES_COLUMN_WIDTH, +}; +use crate::LanguageType; +use std::{collections::BTreeMap, fmt, ops, path::PathBuf}; + +/// A struct representing stats about a single blob of code. +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[non_exhaustive] +pub struct CodeStats { + /// The blank lines in the blob. + pub blanks: usize, + /// The lines of code in the blob. + pub code: usize, + /// The lines of comments in the blob. + pub comments: usize, + /// Language blobs that were contained inside this blob. + pub blobs: BTreeMap, +} + +impl CodeStats { + /// Creates a new blank `CodeStats`. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Get the total lines in a blob of code. + #[must_use] + pub fn lines(&self) -> usize { + self.blanks + self.code + self.comments + } + + /// Creates a new `CodeStats` from an existing one with all of the child + /// blobs merged. + #[must_use] + pub fn summarise(&self) -> Self { + let mut summary = self.clone(); + + for (_, stats) in std::mem::take(&mut summary.blobs) { + let child_summary = stats.summarise(); + + summary.blanks += child_summary.blanks; + summary.comments += child_summary.comments; + summary.code += child_summary.code; + } + + summary + } +} + +impl ops::AddAssign for CodeStats { + fn add_assign(&mut self, rhs: Self) { + self.add_assign(&rhs); + } +} + +impl ops::AddAssign<&'_ CodeStats> for CodeStats { + fn add_assign(&mut self, rhs: &'_ CodeStats) { + self.blanks += rhs.blanks; + self.code += rhs.code; + self.comments += rhs.comments; + + for (language, stats) in &rhs.blobs { + *self.blobs.entry(*language).or_default() += stats; + } + } +} + +/// A struct representing the statistics of a file. +#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)] +#[non_exhaustive] +pub struct Report { + /// The code statistics found in the file. + pub stats: CodeStats, + /// File name. + pub name: PathBuf, +} + +impl Report { + /// Create a new `Report` from a [`PathBuf`]. + /// + /// [`PathBuf`]: //doc.rust-lang.org/std/path/struct.PathBuf.html + #[must_use] + pub fn new(name: PathBuf) -> Self { + Self { + name, + ..Self::default() + } + } +} + +impl ops::AddAssign for Report { + fn add_assign(&mut self, rhs: CodeStats) { + self.stats += rhs; + } +} + +#[doc(hidden)] +#[must_use] +pub fn find_char_boundary(s: &str, index: usize) -> usize { + for i in 0..4 { + if s.is_char_boundary(index + i) { + return index + i; + } + } + unreachable!(); +} + +macro_rules! display_stats { + ($f:expr, $this:expr, $name:expr, $max:expr) => { + write!( + $f, + " {: LINES_COLUMN_WIDTH$} {:>CODE_COLUMN_WIDTH$} {:>COMMENTS_COLUMN_WIDTH$} {:>BLANKS_COLUMN_WIDTH$}", + $name, + $this.stats.lines(), + $this.stats.code, + $this.stats.comments, + $this.stats.blanks, + max = $max + ) + }; +} + +impl fmt::Display for Report { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let name = self.name.to_string_lossy(); + let name_length = name.len(); + + // Added 2 to max length to cover wider Files column (see https://github.com/XAMPPRocky/tokei/issues/891). + let max_len = f.width().unwrap_or(27) + 2; + + if name_length <= max_len { + display_stats!(f, self, name, max_len) + } else { + let mut formatted = String::from("|"); + // Add 1 to the index to account for the '|' we add to the output string + let from = find_char_boundary(&name, name_length + 1 - max_len); + formatted.push_str(&name[from..]); + display_stats!(f, self, formatted, max_len) + } + } +} diff --git a/userland/upstream-src/tokei/src/utils/ext.rs b/userland/upstream-src/tokei/src/utils/ext.rs new file mode 100644 index 0000000000..bc526de7d2 --- /dev/null +++ b/userland/upstream-src/tokei/src/utils/ext.rs @@ -0,0 +1,158 @@ +//! Various extensions to Rust std types. + +pub(crate) trait AsciiExt { + fn is_whitespace(&self) -> bool; + fn is_line_ending_whitespace(&self) -> bool; +} + +impl AsciiExt for u8 { + fn is_whitespace(&self) -> bool { + *self == b' ' || (b'\x09'..=b'\x0d').contains(self) + } + + fn is_line_ending_whitespace(&self) -> bool { + *self == b'\n' + } +} + +pub(crate) trait SliceExt { + fn trim_first_and_last_line_of_whitespace(&self) -> &Self; + fn trim_start(&self) -> &Self; + fn trim(&self) -> &Self; + fn contains_slice(&self, needle: &Self) -> bool; +} + +impl SliceExt for [u8] { + fn trim_first_and_last_line_of_whitespace(&self) -> &Self { + let start = self + .iter() + .position(|c| c.is_line_ending_whitespace() || !c.is_whitespace()) + .map_or(0, |i| (i + 1).min(self.len().saturating_sub(1))); + + let end = self + .iter() + .rposition(|c| c.is_line_ending_whitespace() || !c.is_whitespace()) + .map_or_else( + || self.len().saturating_sub(1), + |i| { + // Remove the entire `\r\n` in the case that it was the line ending whitespace + if self[i.saturating_sub(1)] == b'\r' && self[i] == b'\n' { + i - 1 + } else { + i + } + }, + ); + + if self[start..].is_empty() { + return &[]; + } + + &self[start..=end] + } + + fn trim_start(&self) -> &Self { + let length = self.len(); + + if length == 0 { + return self; + } + + let start = match self.iter().position(|c| !c.is_whitespace()) { + Some(start) => start, + None => return &[], + }; + + &self[start..] + } + + fn trim(&self) -> &Self { + let length = self.len(); + + if length == 0 { + return self; + } + + let start = match self.iter().position(|c| !c.is_whitespace()) { + Some(start) => start, + None => return &[], + }; + + let end = match self.iter().rposition(|c| !c.is_whitespace()) { + Some(end) => end.max(start), + _ => length, + }; + + &self[start..=end] + } + + fn contains_slice(&self, needle: &Self) -> bool { + let self_length = self.len(); + let needle_length = needle.len(); + + if needle_length == 0 || needle_length > self_length { + return false; + } else if needle_length == self_length { + return self == needle; + } + + for window in self.windows(needle_length) { + if needle == window { + return true; + } + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use proptest::prelude::*; + + #[test] + fn is_whitespace() { + assert!(b' '.is_whitespace()); + assert!(b'\r'.is_whitespace()); + assert!(b'\n'.is_whitespace()); + } + + #[test] + fn trim() { + assert!([b' ', b' ', b' '].trim().is_empty()); + assert!([b' ', b'\r', b'\n'].trim().is_empty()); + assert!([b'\n'].trim().is_empty()); + assert!([].trim().is_empty()); + + assert_eq!([b'a', b'b'], [b'a', b'b'].trim()); + assert_eq!([b'h', b'i'], [b' ', b'h', b'i'].trim()); + assert_eq!([b'h', b'i'], [b'h', b'i', b' '].trim()); + assert_eq!([b'h', b'i'], [b' ', b'h', b'i', b' '].trim()); + } + + #[test] + fn contains() { + assert!([1, 2, 3, 4, 5].contains_slice(&[1, 2, 3, 4, 5])); + assert!([1, 2, 3, 4, 5].contains_slice(&[1, 2, 3])); + assert!([1, 2, 3, 4, 5].contains_slice(&[3, 4, 5])); + assert!([1, 2, 3, 4, 5].contains_slice(&[2, 3, 4])); + assert!(![1, 2, 3, 4, 5].contains_slice(&[])); + } + + #[test] + fn trim_first_and_last_line_of_whitespace_edge_cases() { + assert_eq!(b"", b"\ra ".trim_first_and_last_line_of_whitespace()); + assert_eq!(b"a", b"\r\na ".trim_first_and_last_line_of_whitespace()); + + assert_eq!(b" ", b" ".trim_first_and_last_line_of_whitespace()); + } + + proptest! { + #[test] + fn trim_first_and_last_line_of_whitespace_doesnt_panic(input: Vec) { + let _ = &input.trim_first_and_last_line_of_whitespace(); + } + } +} diff --git a/userland/upstream-src/tokei/src/utils/fs.rs b/userland/upstream-src/tokei/src/utils/fs.rs new file mode 100644 index 0000000000..966115f5f7 --- /dev/null +++ b/userland/upstream-src/tokei/src/utils/fs.rs @@ -0,0 +1,486 @@ +use std::{collections::BTreeMap, path::Path}; + +use ignore::{overrides::OverrideBuilder, DirEntry, WalkBuilder, WalkState::Continue}; + +use rayon::prelude::*; + +use crate::{ + config::Config, + language::{Language, LanguageType}, +}; + +const IGNORE_FILE: &str = ".tokeignore"; + +pub fn get_all_files>( + paths: &[A], + ignored_directories: &[&str], + languages: &mut BTreeMap, + config: &Config, +) { + let languages = parking_lot::Mutex::new(languages); + let (tx, rx) = crossbeam_channel::unbounded(); + + let mut paths = paths.iter(); + let mut walker = WalkBuilder::new(paths.next().unwrap()); + + for path in paths { + walker.add(path); + } + + if !ignored_directories.is_empty() { + let mut overrides = OverrideBuilder::new("."); + + for ignored in ignored_directories { + rs_error!(overrides.add(&format!("!{}", ignored))); + } + + walker.overrides(overrides.build().expect("Excludes provided were invalid")); + } + + let ignore = config.no_ignore.map(|b| !b).unwrap_or(true); + let ignore_dot = ignore && config.no_ignore_dot.map(|b| !b).unwrap_or(true); + let ignore_vcs = ignore && config.no_ignore_vcs.map(|b| !b).unwrap_or(true); + + // Custom ignore files always work even if the `ignore` option is false, + // so we only add if that option is not present. + if ignore_dot { + walker.add_custom_ignore_filename(IGNORE_FILE); + } + + walker + .git_exclude(ignore_vcs) + .git_global(ignore_vcs) + .git_ignore(ignore_vcs) + .hidden(config.hidden.map(|b| !b).unwrap_or(true)) + .ignore(ignore_dot) + .parents(ignore && config.no_ignore_parent.map(|b| !b).unwrap_or(true)); + + walker.build_parallel().run(move || { + let tx = tx.clone(); + Box::new(move |entry| { + let entry = match entry { + Ok(entry) => entry, + Err(error) => { + use ignore::Error; + if let Error::WithDepth { err: ref error, .. } = error { + if let Error::WithPath { + ref path, + err: ref error, + } = **error + { + error!("{} reading {}", error, path.display()); + return Continue; + } + } + error!("{}", error); + return Continue; + } + }; + + if entry.file_type().map_or(false, |ft| ft.is_file()) { + tx.send(entry).unwrap(); + } + + Continue + }) + }); + + let rx_iter = rx + .into_iter() + .par_bridge() + .filter_map(|e| LanguageType::from_path(e.path(), config).map(|l| (e, l))); + + let process = |(entry, language): (DirEntry, LanguageType)| { + let result = language.parse(entry.into_path(), config); + let mut lock = languages.lock(); + let entry = lock.entry(language).or_insert_with(Language::new); + match result { + Ok(stats) => { + let func = config.for_each_fn; + if let Some(f) = func { + f(language, stats.clone()) + }; + entry.add_report(stats) + } + Err((error, path)) => { + entry.mark_inaccurate(); + error!("Error reading {}:\n{}", path.display(), error); + } + } + }; + + if let Some(types) = config.types.as_deref() { + rx_iter.filter(|(_, l)| types.contains(l)).for_each(process) + } else { + rx_iter.for_each(process) + } +} + +pub(crate) fn get_extension(path: &Path) -> Option { + path.extension().map(|e| e.to_string_lossy().to_lowercase()) +} + +pub(crate) fn get_filename(path: &Path) -> Option { + path.file_name().map(|e| e.to_string_lossy().to_lowercase()) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::TempDir; + + use super::IGNORE_FILE; + use crate::{ + config::Config, + language::{languages::Languages, LanguageType}, + }; + + const FILE_CONTENTS: &[u8] = b"fn main() {}"; + const FILE_NAME: &str = "main.rs"; + const IGNORE_PATTERN: &str = "*.rs"; + const LANGUAGE: &LanguageType = &LanguageType::Rust; + + #[test] + fn ignore_directory_with_extension() { + let mut languages = Languages::new(); + let tmp_dir = TempDir::new().expect("Couldn't create temp dir"); + let path_name = tmp_dir.path().join("directory.rs"); + + fs::create_dir(path_name).expect("Couldn't create directory.rs within temp"); + + super::get_all_files( + &[tmp_dir.into_path().to_str().unwrap()], + &[], + &mut languages, + &Config::default(), + ); + + assert!(languages.get(LANGUAGE).is_none()); + } + + #[test] + fn hidden() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + fs::write(dir.path().join(".hidden.rs"), FILE_CONTENTS).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + config.hidden = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } + + #[test] + fn no_ignore_implies_dot() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + fs::write(dir.path().join(".ignore"), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + config.no_ignore = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } + + #[test] + fn no_ignore_implies_vcs_gitignore() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + git2::Repository::init(dir.path()).expect("Couldn't create git repo."); + + fs::write(dir.path().join(".gitignore"), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + config.no_ignore = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } + + #[test] + fn no_ignore_parent() { + let parent_dir = TempDir::new().expect("Couldn't create temp dir."); + let child_dir = parent_dir.path().join("child/"); + let mut config = Config::default(); + let mut languages = Languages::new(); + + fs::create_dir_all(&child_dir) + .unwrap_or_else(|_| panic!("Couldn't create {:?}", child_dir)); + fs::write(parent_dir.path().join(".ignore"), IGNORE_PATTERN) + .expect("Couldn't create .gitignore."); + fs::write(child_dir.join(FILE_NAME), FILE_CONTENTS).expect("Couldn't create child.rs"); + + super::get_all_files( + &[child_dir.as_path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + config.no_ignore_parent = Some(true); + + super::get_all_files( + &[child_dir.as_path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } + + #[test] + fn no_ignore_dot() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + fs::write(dir.path().join(".ignore"), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + config.no_ignore_dot = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } + + #[test] + fn no_ignore_dot_still_vcs_gitignore() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + git2::Repository::init(dir.path()).expect("Couldn't create git repo."); + + fs::write(dir.path().join(".gitignore"), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + config.no_ignore_dot = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + } + + #[test] + fn no_ignore_dot_includes_custom_ignore() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + fs::write(dir.path().join(IGNORE_FILE), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + config.no_ignore_dot = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } + + #[test] + fn no_ignore_vcs_gitignore() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + git2::Repository::init(dir.path()).expect("Couldn't create git repo."); + + fs::write(dir.path().join(".gitignore"), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + config.no_ignore_vcs = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } + + #[test] + fn no_ignore_vcs_gitignore_still_dot() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + fs::write(dir.path().join(".ignore"), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + config.no_ignore_vcs = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + } + + #[test] + fn no_ignore_vcs_gitexclude() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let mut config = Config::default(); + let mut languages = Languages::new(); + + git2::Repository::init(dir.path()).expect("Couldn't create git repo."); + + fs::write(dir.path().join(".git/info/exclude"), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + config.no_ignore_vcs = Some(true); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } + + #[test] + fn custom_ignore() { + let dir = TempDir::new().expect("Couldn't create temp dir."); + let config = Config::default(); + let mut languages = Languages::new(); + + git2::Repository::init(dir.path()).expect("Couldn't create git repo."); + + fs::write(dir.path().join(IGNORE_FILE), IGNORE_PATTERN).unwrap(); + fs::write(dir.path().join(FILE_NAME), FILE_CONTENTS).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_none()); + + fs::remove_file(dir.path().join(IGNORE_FILE)).unwrap(); + + super::get_all_files( + &[dir.path().to_str().unwrap()], + &[], + &mut languages, + &config, + ); + + assert!(languages.get(LANGUAGE).is_some()); + } +} diff --git a/userland/upstream-src/tokei/src/utils/macros.rs b/userland/upstream-src/tokei/src/utils/macros.rs new file mode 100644 index 0000000000..cdf93edea2 --- /dev/null +++ b/userland/upstream-src/tokei/src/utils/macros.rs @@ -0,0 +1,102 @@ +#![allow(unused_macros)] + +macro_rules! opt_warn { + ($option:expr, $message:expr) => { + match $option { + Some(result) => result, + None => { + warn!($message); + continue; + } + } + }; +} + +macro_rules! rs_warn { + ($result:expr, $message: expr) => { + match $result { + Ok(result) => result, + Err(error) => { + warn!("{}", error); + continue; + } + } + }; +} + +macro_rules! opt_error { + ($option:expr, $message:expr) => { + match $option { + Some(result) => result, + None => { + error!($message); + continue; + } + } + }; +} + +macro_rules! rs_error { + ($result:expr) => { + match $result { + Ok(result) => result, + Err(error) => { + error!("{}", error); + continue; + } + } + }; +} + +macro_rules! opt_ret_warn { + ($option:expr, $message:expr) => { + match $option { + Some(result) => result, + None => { + warn!($message); + return None; + } + } + }; +} + +macro_rules! rs_ret_warn { + ($result:expr, $message: expr) => { + match $result { + Ok(result) => result, + Err(error) => { + warn!("{}", error); + return None; + } + } + }; +} + +macro_rules! opt_ret_error { + ($option:expr, $message:expr) => { + match $option { + Some(result) => result, + None => { + error!($message); + return None; + } + } + }; +} + +macro_rules! rs_ret_error { + ($result:expr) => { + match $result { + Ok(result) => result, + Err(error) => { + error!("{}", error); + return None; + } + } + }; +} + +macro_rules! debug { + ($fmt:expr) => (if cfg!(debug_assertions) {println!($fmt)}); + ($fmt:expr, $($arg:tt)*) => (if cfg!(debug_assertions) {println!($fmt, $($arg)*)}); +} diff --git a/userland/upstream-src/tokei/src/utils/mod.rs b/userland/upstream-src/tokei/src/utils/mod.rs new file mode 100644 index 0000000000..a3ce44911d --- /dev/null +++ b/userland/upstream-src/tokei/src/utils/mod.rs @@ -0,0 +1,4 @@ +#[macro_use] +mod macros; +pub(crate) mod ext; +pub mod fs;