Skip to content

Commit b9d233f

Browse files
authored
mktemp: handle invalid UTF-8 in suffix gracefully (#10818)
1 parent a9136fb commit b9d233f

File tree

2 files changed

+49
-13
lines changed

2 files changed

+49
-13
lines changed

src/uu/mktemp/src/mktemp.rs

Lines changed: 29 additions & 13 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,27 +214,39 @@ 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.
223235
if options.suffix.is_some() && !template_str.ends_with('X') {
224-
return Err(MkTempError::MustEndInX(template_str.to_string()));
236+
return Err(MkTempError::MustEndInX(template_str.clone()));
225237
}
226238

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
234246
.chars()
235247
.take(template_str.len())
236248
.collect::<String>(),
237-
None => template_str.to_string(),
249+
None => template_str.clone(),
238250
};
239251
return Err(MkTempError::TooFewXs(s));
240252
};
@@ -249,11 +261,11 @@ impl Params {
249261
let prefix_path = Path::new(&prefix_from_option).join(prefix_from_template);
250262
if options.treat_as_template && prefix_from_template.contains(MAIN_SEPARATOR) {
251263
return Err(MkTempError::PrefixContainsDirSeparator(
252-
template_str.to_string(),
264+
template_str.clone(),
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(template_str.clone().into()));
257269
}
258270

259271
// Split the parent directory from the file part of the prefix.
@@ -271,7 +283,7 @@ impl Params {
271283
};
272284
let prefix = match prefix_path.file_name() {
273285
None => String::new(),
274-
Some(f) => f.to_str().unwrap().to_string(),
286+
Some(f) => f.to_string_lossy().to_string(),
275287
};
276288
(directory, prefix)
277289
}
@@ -281,7 +293,10 @@ impl Params {
281293
//
282294
// For example, if the suffix command-line argument is ".txt" and
283295
// the template is "XXXabc", then `suffix` is "abc.txt".
284-
let suffix_from_option = options.suffix.unwrap_or_default();
296+
let suffix_from_option = options
297+
.suffix
298+
.map(|s| s.to_string_lossy().to_string())
299+
.unwrap_or_default();
285300
let suffix_from_template = &template_str[j..];
286301
let suffix = format!("{suffix_from_template}{suffix_from_option}");
287302
if suffix.contains(MAIN_SEPARATOR) {
@@ -445,7 +460,8 @@ pub fn uu_app() -> Command {
445460
Arg::new(OPT_SUFFIX)
446461
.long(OPT_SUFFIX)
447462
.help(translate!("mktemp-help-suffix"))
448-
.value_name("SUFFIX"),
463+
.value_name("SUFFIX")
464+
.value_parser(clap::value_parser!(OsString)),
449465
)
450466
.arg(
451467
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)