mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
test(host): hold every host to keeping a script's executable bit
A save is an overwrite of a file that already exists, and the mode belongs to the file rather than to the write. A host that wrote by creating a fresh file, or by sending the far side an explicit set of attributes, would turn a script into something the shell no longer runs — silently, on a save the user made for an unrelated reason, with nothing in the UI to report it. Both hosts already get this right; nothing here is a fix. It is the case that keeps them right, and it runs against the remote link too, where the write is a control request rather than a syscall. Checked by making the local write a remove-then-create: the case fails, as it should. The two sandbox hooks follow the existing symlink one — a host with no notion of an executable bit returns None and the case skips.
This commit is contained in:
@@ -14,6 +14,18 @@ pub trait Sandbox {
|
||||
let _ = (target, link);
|
||||
None
|
||||
}
|
||||
|
||||
/// Turns `p`'s executable bit on, or `None` where the idea does not apply.
|
||||
fn set_executable(&self, p: &Path) -> Option<io::Result<()>> {
|
||||
let _ = p;
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether `p` is executable, or `None` where the idea does not apply.
|
||||
fn is_executable(&self, p: &Path) -> Option<io::Result<bool>> {
|
||||
let _ = p;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
@@ -39,6 +51,7 @@ macro_rules! for_each_host_case {
|
||||
read_file_on_a_dir_errors,
|
||||
write_file_creates_and_overwrites,
|
||||
write_file_reports_its_own_metadata,
|
||||
write_file_keeps_the_executable_bit,
|
||||
write_file_to_missing_parent_errors,
|
||||
create_file_new_rejects_existing,
|
||||
create_dir_non_recursive_needs_parent,
|
||||
@@ -434,6 +447,36 @@ pub fn write_file_reports_its_own_metadata(h: &dyn Host, sb: &dyn Sandbox) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Saving a file must not cost it its executable bit.
|
||||
///
|
||||
/// A save is an overwrite of a file that already exists, and the mode belongs
|
||||
/// to the file rather than to the write — so a host that writes by creating a
|
||||
/// fresh file, or by handing the far side an explicit set of attributes, turns
|
||||
/// a script into something the shell will no longer run. Nothing about the
|
||||
/// edit said to do that, and nothing in the UI would report it.
|
||||
pub fn write_file_keeps_the_executable_bit(h: &dyn Host, sb: &dyn Sandbox) {
|
||||
let f = h.join(sb.path(), "run.sh");
|
||||
h.write_file(&f, b"#!/bin/sh\necho before\n").unwrap();
|
||||
|
||||
let Some(marked) = sb.set_executable(&f) else {
|
||||
return;
|
||||
};
|
||||
marked.unwrap();
|
||||
assert!(
|
||||
sb.is_executable(&f)
|
||||
.expect("a sandbox that can set the bit can read it")
|
||||
.unwrap(),
|
||||
"the sandbox could not make the file executable to begin with"
|
||||
);
|
||||
|
||||
h.write_file(&f, b"#!/bin/sh\necho after\n").unwrap();
|
||||
|
||||
assert!(
|
||||
sb.is_executable(&f).expect("still readable").unwrap(),
|
||||
"saving the script took its executable bit away"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn write_file_to_missing_parent_errors(h: &dyn Host, sb: &dyn Sandbox) {
|
||||
let sandbox = sb.path();
|
||||
let parent = h.join(sandbox, "no-such-dir");
|
||||
|
||||
@@ -501,6 +501,35 @@ mod tests {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_executable(&self, p: &Path) -> Option<io::Result<()>> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
Some(std::fs::set_permissions(
|
||||
p,
|
||||
std::fs::Permissions::from_mode(0o755),
|
||||
))
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = p;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_executable(&self, p: &Path) -> Option<io::Result<bool>> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
Some(std::fs::metadata(p).map(|m| m.permissions().mode() & 0o111 != 0))
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = p;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sandbox() -> (SharedHost, TempSandbox) {
|
||||
|
||||
@@ -43,6 +43,35 @@ impl Sandbox for TempSandbox {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_executable(&self, p: &Path) -> Option<io::Result<()>> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
Some(std::fs::set_permissions(
|
||||
p,
|
||||
std::fs::Permissions::from_mode(0o755),
|
||||
))
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = p;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_executable(&self, p: &Path) -> Option<io::Result<bool>> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
Some(std::fs::metadata(p).map(|m| m.permissions().mode() & 0o111 != 0))
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = p;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stdio_host() -> (SharedHost, TempSandbox) {
|
||||
|
||||
Reference in New Issue
Block a user