diff --git a/Cargo.toml b/Cargo.toml index e9b5037..42f8032 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,9 @@ include = [ "Cargo.toml", "Cargo.lock", "README.md", + "docs/install.md", + "docs/cli.md", + "docs/troubleshooting.md", "LICENSE", "NOTICE", "config.example.toml", diff --git a/scripts/build-release-bundle.sh b/scripts/build-release-bundle.sh index ae72dd3..85b3793 100755 --- a/scripts/build-release-bundle.sh +++ b/scripts/build-release-bundle.sh @@ -46,6 +46,7 @@ mkdir -p \ "$stage_dir/$bundle_name/share/bash-completion/completions" \ "$stage_dir/$bundle_name/share/zsh/site-functions" \ "$stage_dir/$bundle_name/share/fish/vendor_completions.d" \ + "$stage_dir/$bundle_name/share/doc/whispers/docs" \ "$stage_dir/$bundle_name/share/doc/whispers" \ "$stage_dir/$bundle_name/share/licenses/whispers" @@ -65,6 +66,12 @@ install -Dm755 "${target_dir}/whispers-rewrite-worker" \ install -Dm644 README.md \ "$stage_dir/$bundle_name/share/doc/whispers/README.md" +install -Dm644 docs/install.md \ + "$stage_dir/$bundle_name/share/doc/whispers/docs/install.md" +install -Dm644 docs/cli.md \ + "$stage_dir/$bundle_name/share/doc/whispers/docs/cli.md" +install -Dm644 docs/troubleshooting.md \ + "$stage_dir/$bundle_name/share/doc/whispers/docs/troubleshooting.md" install -Dm644 config.example.toml \ "$stage_dir/$bundle_name/share/doc/whispers/config.example.toml" install -Dm644 LICENSE \ diff --git a/src/inject/tests.rs b/src/inject/tests.rs index 6044bd2..f1bc9d5 100644 --- a/src/inject/tests.rs +++ b/src/inject/tests.rs @@ -21,7 +21,7 @@ fn run_wl_copy_reports_spawn_failure() { fn run_wl_copy_reports_non_zero_exit() { let err = clipboard::run_wl_copy( "/bin/sh", - &[String::from("-c"), String::from("exit 7")], + &[String::from("-c"), String::from("cat >/dev/null; exit 7")], "hello", ) .expect_err("non-zero exit should fail"); diff --git a/src/setup.rs b/src/setup.rs index 3d7028d..047c7c4 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -54,7 +54,13 @@ impl CloudSetup { } } +fn validate_setup_invoker(uid: libc::uid_t) -> Result<()> { + side_effects::validate_setup_user(uid) +} + pub async fn run_setup(config_path_override: Option<&Path>) -> Result<()> { + validate_setup_invoker(unsafe { libc::geteuid() })?; + let ui = SetupUi::new(); ui.print_header("whispers setup"); ui.blank(); diff --git a/src/setup/side_effects.rs b/src/setup/side_effects.rs index f81d2be..efbdd8e 100644 --- a/src/setup/side_effects.rs +++ b/src/setup/side_effects.rs @@ -143,6 +143,7 @@ pub(super) fn maybe_prewarm_experimental_nemo( } pub(super) fn maybe_setup_injection_access(ui: &SetupUi) -> Result { + validate_setup_user(unsafe { libc::geteuid() })?; let readiness = crate::inject::InjectionReadinessReport::collect(); if readiness.is_ready() || !readiness.has_uinput_issue() { return Ok(InjectionSetupOutcome::default()); @@ -339,11 +340,7 @@ fn current_user_in_group(username: &str, group: &str) -> Result { fn current_username() -> Result { let uid = unsafe { libc::geteuid() }; - if uid == 0 { - return Err(crate::error::WhsprError::Config( - "run `whispers setup` as your normal user, not as root".into(), - )); - } + validate_setup_user(uid)?; current_username_for_uid_with(uid, |uid, passwd, buffer, result| unsafe { libc::getpwuid_r( @@ -356,6 +353,16 @@ fn current_username() -> Result { }) } +pub(super) fn validate_setup_user(uid: libc::uid_t) -> Result<()> { + if uid == 0 { + return Err(crate::error::WhsprError::Config( + "run `whispers setup` as your normal user, not as root".into(), + )); + } + + Ok(()) +} + pub(super) fn current_username_for_uid_with(uid: libc::uid_t, mut lookup: F) -> Result where F: FnMut(libc::uid_t, *mut libc::passwd, &mut Vec, *mut *mut libc::passwd) -> libc::c_int, diff --git a/src/setup/tests.rs b/src/setup/tests.rs index dfcb3bc..2e25ed4 100644 --- a/src/setup/tests.rs +++ b/src/setup/tests.rs @@ -70,6 +70,28 @@ fn group_membership_failures_become_warnings_without_marking_success() { assert!(warning.contains("group add failed")); } +#[test] +fn setup_rejects_root_before_uinput_readiness_short_circuit() { + let err = side_effects::validate_setup_user(0).expect_err("root should be rejected"); + match err { + WhsprError::Config(message) => { + assert!(message.contains("run `whispers setup` as your normal user, not as root")); + } + other => panic!("unexpected error variant: {other:?}"), + } +} + +#[test] +fn setup_rejects_root_before_any_setup_side_effects() { + let err = super::validate_setup_invoker(0).expect_err("root should be rejected up front"); + match err { + WhsprError::Config(message) => { + assert!(message.contains("run `whispers setup` as your normal user, not as root")); + } + other => panic!("unexpected error variant: {other:?}"), + } +} + #[test] fn group_membership_success_marks_logout_as_needed() { let mut outcome = side_effects::InjectionSetupOutcome::default();