Skip to content

Commit 109f6ab

Browse files
committed
fix: address Gemini review round 2
- Change --attachment from comma-separated to ArgAction::Append (multiple --attachment flags, no more fragile split on comma) - Add path canonicalization and regular-file check in read_attachments - Remove unwrap() in base64 folding, use direct byte-index slicing
1 parent 3538f28 commit 109f6ab

File tree

4 files changed

+51
-26
lines changed

4 files changed

+51
-26
lines changed

src/helpers/gmail/forward.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ pub(super) async fn handle_forward(
3636
(orig, Some(t))
3737
};
3838

39-
let attachments = match parse_optional_trimmed(matches, "attachment") {
40-
Some(paths) => read_attachments(&paths)?,
39+
let attachments = match matches.get_many::<String>("attachment") {
40+
Some(paths) => read_attachments(&paths.cloned().collect::<Vec<_>>())?,
4141
None => Vec::new(),
4242
};
4343

src/helpers/gmail/mod.rs

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -313,23 +313,39 @@ pub(super) fn guess_content_type(filename: &str) -> &'static str {
313313
}
314314
}
315315

316-
/// Read attachment files from a comma-separated list of paths.
317-
pub(super) fn read_attachments(paths_csv: &str) -> Result<Vec<Attachment>, GwsError> {
316+
/// Read attachment files from a list of paths.
317+
///
318+
/// Each path is canonicalized to resolve symlinks and `..` components; the
319+
/// function rejects paths that do not point to a regular file.
320+
pub(super) fn read_attachments(paths: &[String]) -> Result<Vec<Attachment>, GwsError> {
318321
let mut attachments = Vec::new();
319-
for path_str in paths_csv.split(',') {
322+
for path_str in paths {
320323
let path_str = path_str.trim();
321324
if path_str.is_empty() {
322325
continue;
323326
}
324327
let path = std::path::Path::new(path_str);
325-
let data = std::fs::read(path).map_err(|e| {
328+
let canonical = path.canonicalize().map_err(|e| {
329+
GwsError::Other(anyhow::anyhow!(
330+
"Failed to resolve attachment path '{}': {}",
331+
path_str,
332+
e
333+
))
334+
})?;
335+
if !canonical.is_file() {
336+
return Err(GwsError::Other(anyhow::anyhow!(
337+
"Attachment path '{}' is not a regular file",
338+
path_str,
339+
)));
340+
}
341+
let data = std::fs::read(&canonical).map_err(|e| {
326342
GwsError::Other(anyhow::anyhow!(
327343
"Failed to read attachment '{}': {}",
328344
path_str,
329345
e
330346
))
331347
})?;
332-
let filename = path
348+
let filename = canonical
333349
.file_name()
334350
.and_then(|n| n.to_str())
335351
.unwrap_or(path_str)
@@ -456,12 +472,17 @@ impl MessageBuilder<'_> {
456472
for att in attachments {
457473
let encoded = STANDARD.encode(&att.data);
458474
// Fold base64 into 76-character lines per RFC 2045.
459-
let folded: String = encoded
460-
.as_bytes()
461-
.chunks(76)
462-
.map(|chunk| std::str::from_utf8(chunk).unwrap())
463-
.collect::<Vec<_>>()
464-
.join("\r\n");
475+
// Base64 output is pure ASCII, so byte-index slicing is safe.
476+
let mut folded = String::with_capacity(encoded.len() + (encoded.len() / 76) * 2);
477+
let mut offset = 0;
478+
while offset < encoded.len() {
479+
if offset > 0 {
480+
folded.push_str("\r\n");
481+
}
482+
let end = (offset + 76).min(encoded.len());
483+
folded.push_str(&encoded[offset..end]);
484+
offset = end;
485+
}
465486

466487
let safe_filename = escape_quoted_string(&att.filename);
467488
message.push_str(&format!(
@@ -630,8 +651,9 @@ impl Helper for GmailHelper {
630651
.arg(
631652
Arg::new("attachment")
632653
.long("attachment")
633-
.help("File path(s) to attach, comma-separated")
634-
.value_name("PATHS"),
654+
.help("File path to attach (may be repeated)")
655+
.value_name("PATH")
656+
.action(ArgAction::Append),
635657
)
636658
.arg(
637659
Arg::new("dry-run")
@@ -646,7 +668,7 @@ EXAMPLES:
646668
gws gmail +send --to alice@example.com --subject 'Hello' --body 'Hi!' --cc bob@example.com
647669
gws gmail +send --to alice@example.com --subject 'Hello' --body 'Hi!' --bcc secret@example.com
648670
gws gmail +send --to alice@example.com --subject 'Report' --body 'See attached' --attachment report.pdf
649-
gws gmail +send --to alice@example.com --subject 'Files' --body 'Multiple' --attachment 'a.pdf,b.zip'
671+
gws gmail +send --to alice@example.com --subject 'Files' --body 'Multiple' --attachment a.pdf --attachment b.zip
650672
651673
TIPS:
652674
Handles RFC 2822 formatting and base64 encoding automatically.
@@ -734,8 +756,9 @@ TIPS:
734756
.arg(
735757
Arg::new("attachment")
736758
.long("attachment")
737-
.help("File path(s) to attach, comma-separated")
738-
.value_name("PATHS"),
759+
.help("File path to attach (may be repeated)")
760+
.value_name("PATH")
761+
.action(ArgAction::Append),
739762
)
740763
.arg(
741764
Arg::new("dry-run")
@@ -803,8 +826,9 @@ TIPS:
803826
.arg(
804827
Arg::new("attachment")
805828
.long("attachment")
806-
.help("File path(s) to attach, comma-separated")
807-
.value_name("PATHS"),
829+
.help("File path to attach (may be repeated)")
830+
.value_name("PATH")
831+
.action(ArgAction::Append),
808832
)
809833
.arg(
810834
Arg::new("remove")
@@ -881,8 +905,9 @@ TIPS:
881905
.arg(
882906
Arg::new("attachment")
883907
.long("attachment")
884-
.help("File path(s) to attach, comma-separated")
885-
.value_name("PATHS"),
908+
.help("File path to attach (may be repeated)")
909+
.value_name("PATH")
910+
.action(ArgAction::Append),
886911
)
887912
.arg(
888913
Arg::new("dry-run")

src/helpers/gmail/reply.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ pub(super) async fn handle_reply(
9494
body: &config.body_text,
9595
};
9696

97-
let attachments = match parse_optional_trimmed(matches, "attachment") {
98-
Some(paths) => read_attachments(&paths)?,
97+
let attachments = match matches.get_many::<String>("attachment") {
98+
Some(paths) => read_attachments(&paths.cloned().collect::<Vec<_>>())?,
9999
None => Vec::new(),
100100
};
101101

src/helpers/gmail/send.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ pub(super) async fn handle_send(
66
) -> Result<(), GwsError> {
77
let config = parse_send_args(matches);
88

9-
let attachments = match parse_optional_trimmed(matches, "attachment") {
10-
Some(paths) => read_attachments(&paths)?,
9+
let attachments = match matches.get_many::<String>("attachment") {
10+
Some(paths) => read_attachments(&paths.cloned().collect::<Vec<_>>())?,
1111
None => Vec::new(),
1212
};
1313

0 commit comments

Comments
 (0)