Skip to content

Commit ba47d02

Browse files
committed
mktemp: handle invalid UTF-8 in suffix gracefully
1 parent 4853729 commit ba47d02

File tree

2 files changed

+48
-10
lines changed

2 files changed

+48
-10
lines changed

src/uu/mktemp/src/mktemp.rs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub struct Options {
101101
pub tmpdir: Option<PathBuf>,
102102

103103
/// The suffix to append to the temporary file, if any.
104-
pub suffix: Option<String>,
104+
pub suffix: Option<OsString>,
105105

106106
/// Whether to treat the template argument as a single file path component.
107107
pub treat_as_template: bool,
@@ -150,7 +150,7 @@ impl Options {
150150
dry_run: matches.get_flag(OPT_DRY_RUN),
151151
quiet: matches.get_flag(OPT_QUIET),
152152
tmpdir,
153-
suffix: matches.get_one::<String>(OPT_SUFFIX).map(String::from),
153+
suffix: matches.get_one::<OsString>(OPT_SUFFIX).cloned(),
154154
treat_as_template: matches.get_flag(OPT_T),
155155
template,
156156
}
@@ -214,9 +214,21 @@ fn find_last_contiguous_block_of_xs(s: &str) -> Option<(usize, usize)> {
214214
impl Params {
215215
fn from(options: Options) -> Result<Self, MkTempError> {
216216
// Convert OsString template to string for processing
217-
let Some(template_str) = options.template.to_str() else {
218-
// For non-UTF-8 templates, return an error
219-
return Err(MkTempError::InvalidTemplate(options.template));
217+
// When using -t flag, be permissive with invalid UTF-8 like GNU mktemp
218+
// Otherwise, maintain strict UTF-8 validation (existing behavior)
219+
let template_str = if options.treat_as_template {
220+
// For -t templates, use lossy conversion for GNU compatibility
221+
options.template.to_string_lossy().into_owned()
222+
} else {
223+
// For regular templates, maintain strict validation
224+
match options.template.to_str() {
225+
Some(s) => s.to_string(),
226+
None => {
227+
return Err(MkTempError::InvalidTemplate(
228+
"template contains invalid UTF-8".into(),
229+
));
230+
}
231+
}
220232
};
221233

222234
// The template argument must end in 'X' if a suffix option is given.
@@ -227,7 +239,7 @@ impl Params {
227239
// Get the start and end indices of the randomized part of the template.
228240
//
229241
// For example, if the template is "abcXXXXyz", then `i` is 3 and `j` is 7.
230-
let Some((i, j)) = find_last_contiguous_block_of_xs(template_str) else {
242+
let Some((i, j)) = find_last_contiguous_block_of_xs(&template_str) else {
231243
let s = match options.suffix {
232244
// If a suffix is specified, the error message includes the template without the suffix.
233245
Some(_) => template_str
@@ -253,7 +265,9 @@ impl Params {
253265
));
254266
}
255267
if tmpdir.is_some() && Path::new(prefix_from_template).is_absolute() {
256-
return Err(MkTempError::InvalidTemplate(template_str.into()));
268+
return Err(MkTempError::InvalidTemplate(
269+
template_str.to_string().into(),
270+
));
257271
}
258272

259273
// Split the parent directory from the file part of the prefix.
@@ -271,7 +285,7 @@ impl Params {
271285
};
272286
let prefix = match prefix_path.file_name() {
273287
None => String::new(),
274-
Some(f) => f.to_str().unwrap().to_string(),
288+
Some(f) => f.to_string_lossy().to_string(),
275289
};
276290
(directory, prefix)
277291
}
@@ -281,7 +295,10 @@ impl Params {
281295
//
282296
// For example, if the suffix command-line argument is ".txt" and
283297
// the template is "XXXabc", then `suffix` is "abc.txt".
284-
let suffix_from_option = options.suffix.unwrap_or_default();
298+
let suffix_from_option = options
299+
.suffix
300+
.map(|s| s.to_string_lossy().to_string())
301+
.unwrap_or_default();
285302
let suffix_from_template = &template_str[j..];
286303
let suffix = format!("{suffix_from_template}{suffix_from_option}");
287304
if suffix.contains(MAIN_SEPARATOR) {
@@ -445,7 +462,8 @@ pub fn uu_app() -> Command {
445462
Arg::new(OPT_SUFFIX)
446463
.long(OPT_SUFFIX)
447464
.help(translate!("mktemp-help-suffix"))
448-
.value_name("SUFFIX"),
465+
.value_name("SUFFIX")
466+
.value_parser(clap::value_parser!(OsString)),
449467
)
450468
.arg(
451469
Arg::new(OPT_P)

tests/by-util/test_mktemp.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,26 @@ fn test_non_utf8_tmpdir_long_option() {
11391139
.succeeds();
11401140
}
11411141

1142+
#[test]
1143+
#[cfg(target_os = "linux")]
1144+
fn test_invalid_utf8_suffix() {
1145+
use std::os::unix::ffi::OsStrExt;
1146+
let (at, mut ucmd) = at_and_ucmd!();
1147+
1148+
// Create invalid UTF-8 bytes for suffix
1149+
// This mimics the GNU test which tests mktemp with bad unicode characters
1150+
let invalid_utf8 = std::ffi::OsStr::from_bytes(b"\xC3|\xED\xBA\xAD");
1151+
1152+
// Test that mktemp handles invalid UTF-8 in suffix gracefully
1153+
// It should succeed and create a file with the lossy conversion of the invalid UTF-8
1154+
ucmd.arg("-p")
1155+
.arg(at.as_string())
1156+
.arg("--suffix")
1157+
.arg(invalid_utf8)
1158+
.arg("tmpXXXXXX")
1159+
.succeeds();
1160+
}
1161+
11421162
#[test]
11431163
#[cfg(target_os = "linux")]
11441164
fn test_non_utf8_tmpdir_directory_creation() {

0 commit comments

Comments
 (0)