use anyhow::{Result, anyhow};
use crate::cli::Cmd;
use s3util_rs::types::StoragePath;
pub fn validate(_line_no: usize, _raw: &str, cmd: &Cmd) -> Result<()> {
if matches!(cmd, Cmd::BatchRun(_)) {
return Err(anyhow!("nested batch-run is not allowed"));
}
if let Cmd::Cp(args) = cmd {
let cp_config = s3util_rs::Config::try_from(args.clone()).map_err(|e| anyhow!("{e}"))?;
if matches!(cp_config.source, StoragePath::Stdio)
|| matches!(cp_config.target, StoragePath::Stdio)
{
return Err(anyhow!(
"stdin/stdout transfers are not allowed inside batch-run"
));
}
}
if let Cmd::Mv(args) = cmd {
let mv_config = s3util_rs::Config::try_from(args.clone()).map_err(|e| anyhow!("{e}"))?;
if matches!(mv_config.source, StoragePath::Stdio)
|| matches!(mv_config.target, StoragePath::Stdio)
{
return Err(anyhow!(
"stdin/stdout transfers are not allowed inside batch-run"
));
}
}
reject_per_line_stdin_config(_line_no, _raw, cmd)?;
reject_per_line_tracing(_line_no, _raw, cmd)?;
Ok(())
}
fn reject_per_line_stdin_config(_line_no: usize, _raw: &str, cmd: &Cmd) -> Result<()> {
let stdin_arg = match cmd {
Cmd::PutBucketPolicy(a) => a.policy.as_deref(),
Cmd::PutBucketLifecycleConfiguration(a) => a.lifecycle_configuration.as_deref(),
Cmd::PutBucketEncryption(a) => a.server_side_encryption_configuration.as_deref(),
Cmd::PutBucketCors(a) => a.cors_configuration.as_deref(),
Cmd::PutPublicAccessBlock(a) => a.public_access_block_configuration.as_deref(),
Cmd::PutBucketWebsite(a) => a.website_configuration.as_deref(),
Cmd::PutBucketLogging(a) => a.bucket_logging_status.as_deref(),
Cmd::PutBucketNotificationConfiguration(a) => a.notification_configuration.as_deref(),
Cmd::PutBucketReplication(a) => a.replication_configuration.as_deref(),
_ => return Ok(()),
};
if stdin_arg == Some("-") {
return Err(anyhow!(
"stdin/stdout transfers are not allowed inside batch-run"
));
}
Ok(())
}
fn reject_per_line_tracing(_line_no: usize, _raw: &str, cmd: &Cmd) -> Result<()> {
macro_rules! check_common {
($c:expr) => {{
if $c.json_tracing
|| $c.aws_sdk_tracing
|| $c.span_events_tracing
|| $c.disable_color_tracing
{
return Err(tracing_error());
}
}};
}
match cmd {
Cmd::BatchRun(_) => {}
Cmd::Ls(boxed) => check_common!(boxed),
Cmd::Clean(boxed) => check_common!(boxed),
Cmd::Sync(boxed) => {
let cfg = s3sync::Config::try_from((**boxed).clone()).map_err(|e| anyhow!("{e}"))?;
if let Some(t) = cfg.tracing_config
&& (t.json_tracing
|| t.aws_sdk_tracing
|| t.span_events_tracing
|| t.disable_color_tracing)
{
return Err(tracing_error());
}
}
Cmd::Cp(a) => check_common!(a.common),
Cmd::Mv(a) => check_common!(a.common),
Cmd::Rename(a) => check_common!(a),
Cmd::Rm(a) => check_common!(a.common),
Cmd::CreateBucket(a) => check_common!(a.common),
Cmd::DeleteBucket(a) => check_common!(a.common),
Cmd::HeadBucket(a) => check_common!(a.common),
Cmd::HeadObject(a) => check_common!(a.common),
Cmd::GetObjectTagging(a) => check_common!(a.common),
Cmd::PutObjectTagging(a) => check_common!(a.common),
Cmd::DeleteObjectTagging(a) => check_common!(a.common),
Cmd::GetBucketTagging(a) => check_common!(a.common),
Cmd::PutBucketTagging(a) => check_common!(a.common),
Cmd::DeleteBucketTagging(a) => check_common!(a.common),
Cmd::GetBucketPolicy(a) => check_common!(a.common),
Cmd::PutBucketPolicy(a) => check_common!(a.common),
Cmd::DeleteBucketPolicy(a) => check_common!(a.common),
Cmd::GetBucketVersioning(a) => check_common!(a.common),
Cmd::PutBucketVersioning(a) => check_common!(a.common),
Cmd::GetBucketLifecycleConfiguration(a) => check_common!(a.common),
Cmd::PutBucketLifecycleConfiguration(a) => check_common!(a.common),
Cmd::DeleteBucketLifecycleConfiguration(a) => check_common!(a.common),
Cmd::GetBucketEncryption(a) => check_common!(a.common),
Cmd::PutBucketEncryption(a) => check_common!(a.common),
Cmd::DeleteBucketEncryption(a) => check_common!(a.common),
Cmd::GetBucketCors(a) => check_common!(a.common),
Cmd::PutBucketCors(a) => check_common!(a.common),
Cmd::DeleteBucketCors(a) => check_common!(a.common),
Cmd::GetPublicAccessBlock(a) => check_common!(a.common),
Cmd::PutPublicAccessBlock(a) => check_common!(a.common),
Cmd::DeletePublicAccessBlock(a) => check_common!(a.common),
Cmd::GetBucketWebsite(a) => check_common!(a.common),
Cmd::PutBucketWebsite(a) => check_common!(a.common),
Cmd::DeleteBucketWebsite(a) => check_common!(a.common),
Cmd::GetBucketLogging(a) => check_common!(a.common),
Cmd::PutBucketLogging(a) => check_common!(a.common),
Cmd::GetBucketNotificationConfiguration(a) => check_common!(a.common),
Cmd::PutBucketNotificationConfiguration(a) => check_common!(a.common),
Cmd::GetBucketReplication(a) => check_common!(a.common),
Cmd::PutBucketReplication(a) => check_common!(a.common),
Cmd::DeleteBucketReplication(a) => check_common!(a.common),
Cmd::GetBucketAccelerateConfiguration(a) => check_common!(a.common),
Cmd::PutBucketAccelerateConfiguration(a) => check_common!(a.common),
Cmd::GetBucketRequestPayment(a) => check_common!(a.common),
Cmd::PutBucketRequestPayment(a) => check_common!(a.common),
Cmd::GetBucketPolicyStatus(a) => check_common!(a.common),
Cmd::RestoreObject(a) => check_common!(a.common),
Cmd::Presign(a) => check_common!(a.common),
}
Ok(())
}
fn tracing_error() -> anyhow::Error {
anyhow!(
"tracing flags are not allowed inside batch-run lines; \
pass them to batch-run itself, e.g. `s7cmd batch-run --aws-sdk-tracing < cmds.txt`"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Cli;
use clap::Parser;
fn parse_cmd(argv: &[&str]) -> Cmd {
Cli::try_parse_from(argv).unwrap().command.unwrap()
}
#[test]
fn rejects_nested_batch_run() {
let cmd = parse_cmd(&["s7cmd", "batch-run", "-"]);
let err = validate(7, "batch-run -", &cmd).unwrap_err();
assert!(err.to_string().contains("nested batch-run"));
}
#[test]
fn rejects_cp_with_stdio_target() {
let cmd = parse_cmd(&["s7cmd", "cp", "s3://b/k", "-"]);
let err = validate(3, "cp s3://b/k -", &cmd).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("stdin/stdout"), "msg: {msg}");
}
#[test]
fn rejects_cp_with_stdio_source() {
let cmd = parse_cmd(&["s7cmd", "cp", "-", "s3://b/k"]);
let err = validate(1, "cp - s3://b/k", &cmd).unwrap_err();
assert!(err.to_string().contains("stdin/stdout"));
}
#[test]
fn allows_normal_cp() {
let cmd = parse_cmd(&["s7cmd", "cp", "s3://b/a", "s3://b/b"]);
validate(1, "cp s3://b/a s3://b/b", &cmd).unwrap();
}
#[test]
fn rejects_mv_with_stdio_target() {
let cmd = parse_cmd(&["s7cmd", "mv", "s3://b/k", "-"]);
let err = validate(4, "mv s3://b/k -", &cmd).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("stdin/stdout"), "msg: {msg}");
}
#[test]
fn rejects_mv_with_stdio_source() {
let cmd = parse_cmd(&["s7cmd", "mv", "-", "s3://b/k"]);
let err = validate(2, "mv - s3://b/k", &cmd).unwrap_err();
assert!(err.to_string().contains("stdin/stdout"));
}
#[test]
fn allows_normal_mv() {
let cmd = parse_cmd(&["s7cmd", "mv", "s3://b/a", "s3://b/b"]);
validate(1, "mv s3://b/a s3://b/b", &cmd).unwrap();
}
#[test]
fn rejects_per_line_aws_sdk_tracing() {
let cmd = parse_cmd(&["s7cmd", "head-bucket", "--aws-sdk-tracing", "s3://b"]);
let err = validate(2, "head-bucket --aws-sdk-tracing s3://b", &cmd).unwrap_err();
assert!(err.to_string().contains("tracing flags"));
}
#[test]
fn rejects_per_line_json_tracing_on_cp() {
let cmd = parse_cmd(&["s7cmd", "cp", "--json-tracing", "s3://b/a", "s3://b/b"]);
let err = validate(2, "cp --json-tracing s3://b/a s3://b/b", &cmd).unwrap_err();
assert!(err.to_string().contains("tracing flags"));
}
fn assert_rejects_stdin_dash(argv: &[&str]) {
let cmd = parse_cmd(argv);
let raw = argv[1..].join(" ");
let err = validate(1, &raw, &cmd).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("stdin/stdout"),
"expected stdin/stdout error for argv {argv:?}, got: {msg}"
);
}
#[test]
fn rejects_put_bucket_commands_reading_stdin_dash() {
let cases = [
"put-bucket-policy",
"put-bucket-lifecycle-configuration",
"put-bucket-encryption",
"put-bucket-cors",
"put-public-access-block",
"put-bucket-website",
"put-bucket-logging",
"put-bucket-notification-configuration",
"put-bucket-replication",
];
for sub in cases {
assert_rejects_stdin_dash(&["s7cmd", sub, "s3://b", "-"]);
}
}
#[test]
fn allows_put_bucket_policy_with_file_path() {
let cmd = parse_cmd(&["s7cmd", "put-bucket-policy", "s3://b", "/tmp/policy.json"]);
validate(1, "put-bucket-policy s3://b /tmp/policy.json", &cmd).unwrap();
}
#[test]
fn allows_create_bucket_without_tracing_flags() {
let cmd = parse_cmd(&["s7cmd", "create-bucket", "s3://b"]);
validate(1, "create-bucket s3://b", &cmd).unwrap();
}
fn assert_rejects_tracing(argv: &[&str]) {
let cmd = parse_cmd(argv);
let raw = argv[1..].join(" ");
let err = validate(1, &raw, &cmd).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("tracing flags"),
"expected tracing-flags error for argv {argv:?}, got: {msg}"
);
}
#[test]
fn rejects_per_line_tracing_on_ls() {
assert_rejects_tracing(&["s7cmd", "ls", "--json-tracing", "s3://b"]);
}
#[test]
fn rejects_per_line_tracing_on_clean() {
assert_rejects_tracing(&["s7cmd", "clean", "--force", "--json-tracing", "s3://b"]);
}
#[test]
fn rejects_per_line_tracing_on_clean_aws_sdk_tracing() {
assert_rejects_tracing(&["s7cmd", "clean", "--force", "--aws-sdk-tracing", "s3://b"]);
}
#[test]
fn rejects_per_line_tracing_on_clean_span_events_tracing() {
assert_rejects_tracing(&[
"s7cmd",
"clean",
"--force",
"--span-events-tracing",
"s3://b",
]);
}
#[test]
fn rejects_per_line_tracing_on_clean_disable_color_tracing() {
assert_rejects_tracing(&[
"s7cmd",
"clean",
"--force",
"--disable-color-tracing",
"s3://b",
]);
}
#[test]
fn rejects_per_line_tracing_on_sync() {
assert_rejects_tracing(&["s7cmd", "sync", "--json-tracing", "s3://b1", "s3://b2"]);
}
#[test]
fn rejects_per_line_tracing_on_sync_aws_sdk_tracing() {
assert_rejects_tracing(&["s7cmd", "sync", "--aws-sdk-tracing", "s3://b1", "s3://b2"]);
}
#[test]
fn rejects_per_line_tracing_on_sync_span_events_tracing() {
assert_rejects_tracing(&[
"s7cmd",
"sync",
"--span-events-tracing",
"s3://b1",
"s3://b2",
]);
}
#[test]
fn rejects_per_line_tracing_on_sync_disable_color_tracing() {
assert_rejects_tracing(&[
"s7cmd",
"sync",
"--disable-color-tracing",
"s3://b1",
"s3://b2",
]);
}
#[test]
fn rejects_invalid_sync_config_at_validate_time() {
let cmd = parse_cmd(&["s7cmd", "sync", "/tmp/src", "/tmp/dst"]);
let err = validate(4, "sync /tmp/src /tmp/dst", &cmd).unwrap_err();
let msg = err.to_string();
assert!(
!msg.is_empty(),
"expected a non-empty validate error, got empty"
);
}
#[test]
fn rejects_per_line_tracing_on_mv() {
assert_rejects_tracing(&["s7cmd", "mv", "--json-tracing", "s3://b/a", "s3://b/b"]);
}
#[test]
fn rejects_per_line_tracing_for_all_common_client_variants() {
let bucket_only = [
"rm",
"create-bucket",
"delete-bucket",
"head-bucket",
"get-bucket-tagging",
"delete-bucket-tagging",
"get-bucket-policy",
"delete-bucket-policy",
"get-bucket-versioning",
"get-bucket-lifecycle-configuration",
"delete-bucket-lifecycle-configuration",
"get-bucket-encryption",
"delete-bucket-encryption",
"get-bucket-cors",
"delete-bucket-cors",
"get-public-access-block",
"delete-public-access-block",
"get-bucket-website",
"delete-bucket-website",
"get-bucket-logging",
"get-bucket-notification-configuration",
"get-bucket-replication",
"delete-bucket-replication",
"get-bucket-accelerate-configuration",
"get-bucket-request-payment",
"get-bucket-policy-status",
];
for sub in bucket_only {
let target: &str = if sub == "rm" { "s3://b/k" } else { "s3://b" };
assert_rejects_tracing(&["s7cmd", sub, "--json-tracing", target]);
}
let object_keyed = [
"head-object",
"get-object-tagging",
"delete-object-tagging",
"restore-object",
"presign",
];
for sub in object_keyed {
assert_rejects_tracing(&["s7cmd", sub, "--json-tracing", "s3://b/k"]);
}
assert_rejects_tracing(&[
"s7cmd",
"put-bucket-versioning",
"--json-tracing",
"--enabled",
"s3://b",
]);
assert_rejects_tracing(&[
"s7cmd",
"put-bucket-accelerate-configuration",
"--json-tracing",
"--enabled",
"s3://b",
]);
assert_rejects_tracing(&[
"s7cmd",
"put-bucket-request-payment",
"--json-tracing",
"--requester",
"s3://b",
]);
assert_rejects_tracing(&[
"s7cmd",
"put-bucket-tagging",
"--json-tracing",
"--tagging",
"k=v",
"s3://b",
]);
assert_rejects_tracing(&[
"s7cmd",
"put-object-tagging",
"--json-tracing",
"--tagging",
"k=v",
"s3://b/k",
]);
let put_with_file = [
"put-bucket-policy",
"put-bucket-lifecycle-configuration",
"put-bucket-encryption",
"put-bucket-cors",
"put-public-access-block",
"put-bucket-website",
"put-bucket-logging",
"put-bucket-notification-configuration",
"put-bucket-replication",
];
for sub in put_with_file {
assert_rejects_tracing(&["s7cmd", sub, "--json-tracing", "s3://b", "/dev/null"]);
}
}
}