1use crate::crypto::{CHECKSUM_LEN, KDFALG, KEYNUMLEN, PKALG, SALT_LEN};
16use crate::error::{Error, Result};
17use base64ct::{Base64, Encoding as _};
18use core::str;
19use memchr::memchr;
20use static_assertions::assert_eq_size;
21use std::fs::File;
22use std::io::stdin;
23use std::io::stdout;
24use std::io::{Read, Write};
25use std::path::Component;
26use std::path::Path;
27use zeroize::Zeroize;
28use zeroize::Zeroizing;
29
30pub const COMMENTHDR: &str = "untrusted comment: ";
32pub const MAX_COMMENT_LEN: usize = 1024;
34
35#[repr(C)]
39#[derive(Debug)]
40pub struct EncKey {
41 pub pkalg: [u8; 2],
43 pub kdfalg: [u8; 2],
45 pub kdfrounds: u32,
47 pub salt: [u8; SALT_LEN],
49 pub checksum: [u8; CHECKSUM_LEN],
51 pub keynum: [u8; KEYNUMLEN],
53 pub seckey: Zeroizing<[u8; 64]>,
55}
56
57impl Zeroize for EncKey {
58 fn zeroize(&mut self) {
59 self.pkalg.zeroize();
60 self.kdfalg.zeroize();
61 self.kdfrounds.zeroize();
62 self.salt.zeroize();
63 self.checksum.zeroize();
64 self.keynum.zeroize();
65 self.seckey.zeroize();
66 }
67}
68
69impl Drop for EncKey {
70 fn drop(&mut self) {
71 self.zeroize();
72 }
73}
74
75#[repr(C)]
77#[derive(Debug, Clone, Copy)]
78pub struct PubKey {
79 pub pkalg: [u8; 2],
81 pub keynum: [u8; KEYNUMLEN],
83 pub pubkey: [u8; 32],
85}
86
87#[derive(Debug, Clone, Copy)]
89pub struct Sig {
90 pub pkalg: [u8; 2],
92 pub keynum: [u8; KEYNUMLEN],
94 pub sig: [u8; 64],
96}
97
98impl EncKey {
99 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
107 let pkalg = bytes
108 .get(0..2)
109 .ok_or(Error::InvalidKeyLength)?
110 .try_into()
111 .map_err(|_e| Error::InvalidKeyLength)?;
112 let kdfalg = bytes
113 .get(2..4)
114 .ok_or(Error::InvalidKeyLength)?
115 .try_into()
116 .map_err(|_e| Error::InvalidKeyLength)?;
117 let kdfrounds_bytes = bytes.get(4..8).ok_or(Error::InvalidKeyLength)?;
118 let kdfrounds = u32::from_be_bytes(
119 kdfrounds_bytes
120 .try_into()
121 .map_err(|_e| Error::InvalidKeyLength)?,
122 );
123 let salt = bytes
124 .get(8..24)
125 .ok_or(Error::InvalidKeyLength)?
126 .try_into()
127 .map_err(|_e| Error::InvalidKeyLength)?;
128 let checksum = bytes
129 .get(24..32)
130 .ok_or(Error::InvalidKeyLength)?
131 .try_into()
132 .map_err(|_e| Error::InvalidKeyLength)?;
133 let keynum = bytes
134 .get(32..40)
135 .ok_or(Error::InvalidKeyLength)?
136 .try_into()
137 .map_err(|_e| Error::InvalidKeyLength)?;
138 let seckey = Zeroizing::new(
139 bytes
140 .get(40..104)
141 .ok_or(Error::InvalidKeyLength)?
142 .try_into()
143 .map_err(|_e| Error::InvalidKeyLength)?,
144 );
145
146 if bytes.len() != 104 {
148 return Err(Error::InvalidKeyLength);
149 }
150
151 if pkalg != PKALG {
152 return Err(Error::UnsupportedPkAlgo);
153 }
154 if kdfalg != KDFALG {
155 return Err(Error::UnsupportedKdfAlgo);
156 }
157
158 Ok(Self {
159 pkalg,
160 kdfalg,
161 kdfrounds,
162 salt,
163 checksum,
164 keynum,
165 seckey,
166 })
167 }
168
169 #[must_use]
171 pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
172 let mut out = Zeroizing::new(Vec::with_capacity(104));
173 out.extend_from_slice(&self.pkalg);
174 out.extend_from_slice(&self.kdfalg);
175 out.extend_from_slice(&self.kdfrounds.to_be_bytes());
176 out.extend_from_slice(&self.salt);
177 out.extend_from_slice(&self.checksum);
178 out.extend_from_slice(&self.keynum);
179 out.extend_from_slice(self.seckey.as_ref());
180 out
181 }
182}
183
184impl PubKey {
185 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
192 let pkalg = bytes
193 .get(0..2)
194 .ok_or(Error::InvalidKeyLength)?
195 .try_into()
196 .map_err(|_e| Error::InvalidKeyLength)?;
197 let keynum = bytes
198 .get(2..10)
199 .ok_or(Error::InvalidKeyLength)?
200 .try_into()
201 .map_err(|_e| Error::InvalidKeyLength)?;
202 let pubkey = bytes
203 .get(10..42)
204 .ok_or(Error::InvalidKeyLength)?
205 .try_into()
206 .map_err(|_e| Error::InvalidKeyLength)?;
207
208 if bytes.len() != 42 {
209 return Err(Error::InvalidKeyLength);
210 }
211
212 if pkalg != PKALG {
213 return Err(Error::UnsupportedPkAlgo);
214 }
215
216 Ok(Self {
217 pkalg,
218 keynum,
219 pubkey,
220 })
221 }
222
223 #[must_use]
225 pub fn to_bytes(&self) -> Vec<u8> {
226 let mut out = Vec::with_capacity(42);
227 out.extend_from_slice(&self.pkalg);
228 out.extend_from_slice(&self.keynum);
229 out.extend_from_slice(&self.pubkey);
230 out
231 }
232}
233
234impl Sig {
235 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
242 let pkalg = bytes
243 .get(0..2)
244 .ok_or(Error::InvalidKeyLength)?
245 .try_into()
246 .map_err(|_e| Error::InvalidKeyLength)?;
247 let keynum = bytes
248 .get(2..10)
249 .ok_or(Error::InvalidKeyLength)?
250 .try_into()
251 .map_err(|_e| Error::InvalidKeyLength)?;
252 let sig = bytes
253 .get(10..74)
254 .ok_or(Error::InvalidKeyLength)?
255 .try_into()
256 .map_err(|_e| Error::InvalidKeyLength)?;
257
258 if bytes.len() != 74 {
259 return Err(Error::InvalidKeyLength);
260 }
261
262 if pkalg != PKALG {
263 return Err(Error::UnsupportedPkAlgo);
264 }
265
266 Ok(Self { pkalg, keynum, sig })
267 }
268
269 #[must_use]
271 pub fn to_bytes(&self) -> Vec<u8> {
272 let mut out = Vec::with_capacity(74);
273 out.extend_from_slice(&self.pkalg);
274 out.extend_from_slice(&self.keynum);
275 out.extend_from_slice(&self.sig);
276 out
277 }
278}
279
280pub fn parse_stream<F, R, T>(mut reader: R, parse_fn: F) -> Result<(T, Vec<u8>)>
293where
294 R: Read,
295 F: Fn(&[u8]) -> Result<T>,
296{
297 const HEADER_LIMIT: usize = 4096;
302 let mut header_buf = vec![0_u8; HEADER_LIMIT];
303
304 let mut total_read = 0;
306 while total_read < HEADER_LIMIT {
307 let n = reader
308 .read(&mut header_buf[total_read..])
309 .map_err(Error::Io)?;
310 if n == 0 {
311 break;
312 }
313 total_read = total_read.checked_add(n).ok_or(Error::Overflow)?;
314 }
315 header_buf.truncate(total_read);
316
317 let n1 = memchr(b'\n', &header_buf).ok_or(Error::InvalidCommentHeader)?;
319 let header_bytes = &header_buf[..n1];
320
321 let prefix = COMMENTHDR.as_bytes();
323 if !header_bytes.starts_with(prefix) {
324 return Err(Error::InvalidCommentHeader);
325 }
326 let comment = header_bytes[prefix.len()..].to_vec();
327
328 let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
329 let n2 = memchr(b'\n', &header_buf[n2_start..])
330 .unwrap_or_else(|| header_buf.len().saturating_sub(n2_start));
331
332 let b64_start = n2_start;
333 let b64_end = b64_start.checked_add(n2).ok_or(Error::Overflow)?;
334
335 if b64_end > header_buf.len() {
336 return Err(Error::InvalidCommentHeader);
337 }
338
339 let b64_bytes = &header_buf[b64_start..b64_end];
340
341 let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
343 let decoded = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
344
345 let obj = parse_fn(&decoded)?;
346 Ok((obj, comment))
347}
348
349pub fn parse<T, F>(path: &Path, parse_fn: F) -> Result<(T, Vec<u8>)>
362where
363 F: Fn(&[u8]) -> Result<T>,
364{
365 let reader: Box<dyn Read> = if path.to_str() == Some("-") {
366 Box::new(stdin())
367 } else {
368 Box::new(open(path, false)?)
369 };
370
371 parse_stream(reader, parse_fn)
372}
373
374pub fn write_stream(mut writer: impl Write, comment: &[u8], data: &[u8]) -> Result<()> {
382 let encoded = Base64::encode_string(data);
383
384 let mut content = Vec::new();
385 content.extend_from_slice(COMMENTHDR.as_bytes());
386 content.extend_from_slice(comment);
387 content.push(b'\n');
388 content.extend_from_slice(encoded.as_bytes());
389 content.push(b'\n');
390
391 writer.write_all(&content).map_err(Error::Io)?;
392 Ok(())
393}
394
395pub fn write(path: &Path, comment: &[u8], data: &[u8]) -> Result<()> {
403 let writer: Box<dyn Write> = if path.to_str() == Some("-") {
404 Box::new(stdout())
405 } else {
406 Box::new(open(path, true)?)
407 };
408
409 write_stream(writer, comment, data)
410}
411
412pub fn open(path: &Path, write: bool) -> Result<File> {
416 if path.components().any(|p| p == Component::ParentDir) {
417 return Err(Error::InvalidPath);
418 }
419 #[cfg(unix)]
420 {
421 safe_name(path)?;
422 }
423 #[cfg(target_os = "linux")]
424 {
425 safe_open(path, write)
426 }
427 #[cfg(not(target_os = "linux"))]
428 {
429 use std::fs::OpenOptions;
430
431 let mut opts = OpenOptions::new();
432 if write {
433 opts.write(true).create_new(true);
434 #[cfg(unix)]
435 {
436 use std::os::unix::fs::OpenOptionsExt;
437 opts.mode(0o600);
438 }
439 } else {
440 opts.read(true);
441 #[cfg(unix)]
442 {
443 use nix::fcntl::OFlag;
444 use std::os::unix::fs::OpenOptionsExt;
445 opts.custom_flags(OFlag::O_NOFOLLOW.bits());
446 }
447 }
448
449 opts.open(path).map_err(Error::Io)
450 }
451}
452
453#[cfg(target_os = "linux")]
454fn safe_open(path: &Path, write: bool) -> Result<File> {
455 use nix::fcntl::{openat2, OFlag, OpenHow, ResolveFlag, AT_FDCWD};
456 use nix::sys::stat::Mode;
457 use std::os::fd::AsRawFd;
458
459 let mut how = OpenHow::new()
460 .resolve(ResolveFlag::RESOLVE_NO_SYMLINKS | ResolveFlag::RESOLVE_NO_MAGICLINKS);
461
462 if write {
463 how = how.flags(OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_EXCL);
465 how = how.mode(Mode::from_bits_truncate(0o600));
466 return Ok(openat2(AT_FDCWD, path, how).map(File::from)?);
467 }
468
469 how = how.flags(OFlag::O_PATH | OFlag::O_NOFOLLOW);
471 let file = openat2(AT_FDCWD, path, how).map(File::from)?;
472 if !file.metadata()?.is_file() {
473 return Err(Error::InvalidPath);
474 }
475
476 how = how.flags(OFlag::O_RDONLY).resolve(ResolveFlag::empty());
478 let path = format!("/proc/thread-self/fd/{}", file.as_raw_fd());
479 let file = openat2(AT_FDCWD, path.as_str(), how).map(File::from)?;
480
481 Ok(file)
482}
483
484#[cfg(unix)]
487fn safe_name(path: &Path) -> Result<()> {
488 use nix::errno::Errno;
489 use std::os::unix::ffi::OsStrExt;
490
491 let name = match path.file_name() {
492 Some(name) => name.as_bytes(),
493 None => return Err(Errno::EILSEQ.into()),
494 };
495
496 if !name
498 .get(0)
499 .map(|&b| is_permitted_initial(b))
500 .unwrap_or(false)
501 {
502 return Err(Errno::EILSEQ.into());
503 }
504
505 let last = name.len().saturating_sub(1);
507 if !name
508 .get(last)
509 .map(|&b| is_permitted_final(b))
510 .unwrap_or(false)
511 {
512 return Err(Errno::EILSEQ.into());
513 }
514
515 if last > 1 && !name[1..last].iter().all(|&b| is_permitted_middle(b)) {
517 return Err(Errno::EILSEQ.into());
518 }
519
520 let name = std::str::from_utf8(name).or(Err(Errno::EILSEQ))?;
522
523 if name
526 .chars()
527 .nth(0)
528 .map(|c| c.is_whitespace())
529 .unwrap_or(false)
530 {
531 return Err(Errno::EILSEQ.into());
532 }
533 if name
534 .chars()
535 .last()
536 .map(|c| c.is_whitespace())
537 .unwrap_or(false)
538 {
539 return Err(Errno::EILSEQ.into());
540 }
541
542 Ok(())
543}
544
545#[cfg(unix)]
546fn is_permitted_initial(b: u8) -> bool {
547 is_permitted_byte(b) && !matches!(b, b'-' | b' ' | b'~')
548}
549
550#[cfg(unix)]
551fn is_permitted_middle(b: u8) -> bool {
552 is_permitted_byte(b)
553}
554
555#[cfg(unix)]
556fn is_permitted_final(b: u8) -> bool {
557 is_permitted_byte(b) && b != b' '
558}
559
560#[cfg(unix)]
561fn is_permitted_byte(b: u8) -> bool {
562 match b {
563 b'*' | b'?' | b'[' | b']' | b'"' | b'<' | b'>' | b'|' | b'(' | b')' | b'&' | b'\''
564 | b'!' | b'\\' | b':' | b'{' | b'}' | b';' | b'$' | b'`' => false,
565 0x20..=0x7E => true,
566 0x80..=0xFE => true,
567 _ => false,
568 }
569}
570
571assert_eq_size!(EncKey, [u8; 104]);
572assert_eq_size!(PubKey, [u8; 42]);
573assert_eq_size!(Sig, [u8; 74]);
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 #[cfg(unix)]
579 use std::ffi::OsStr;
580 use std::fs::OpenOptions;
581 #[cfg(unix)]
582 use std::os::unix::ffi::OsStrExt;
583
584 #[test]
585 fn test_enckey_serialization() -> crate::error::Result<()> {
586 let enc = EncKey {
587 pkalg: PKALG,
588 kdfalg: KDFALG,
589 kdfrounds: 42,
590 salt: [1u8; SALT_LEN],
591 checksum: [2u8; CHECKSUM_LEN],
592 keynum: [3u8; KEYNUMLEN],
593 seckey: Zeroizing::new([4u8; 64]),
594 };
595 let bytes = enc.to_bytes();
596 let enc2 = EncKey::from_bytes(&bytes)?;
597 assert_eq!(enc.kdfrounds, enc2.kdfrounds);
598 assert_eq!(enc.salt, enc2.salt);
599
600 assert!(matches!(
602 EncKey::from_bytes(&bytes[..103]),
603 Err(Error::InvalidKeyLength)
604 ));
605
606 let mut long = bytes.clone();
607 long.push(0);
608 assert!(matches!(
609 EncKey::from_bytes(&long),
610 Err(Error::InvalidKeyLength)
611 ));
612
613 let mut bad_alg = bytes.clone();
615 bad_alg[0] = b'X';
616 assert!(matches!(
617 EncKey::from_bytes(&bad_alg),
618 Err(Error::UnsupportedPkAlgo)
619 ));
620
621 let mut bad_kdf = bytes.clone();
623 bad_kdf[2] = b'X';
624 assert!(matches!(
625 EncKey::from_bytes(&bad_kdf),
626 Err(Error::UnsupportedKdfAlgo)
627 ));
628
629 Ok(())
630 }
631
632 #[test]
633 fn test_pubkey_serialization() -> crate::error::Result<()> {
634 let pubk = PubKey {
635 pkalg: PKALG,
636 keynum: [1u8; KEYNUMLEN],
637 pubkey: [2u8; 32],
638 };
639 let bytes = pubk.to_bytes();
640 let pubk2 = PubKey::from_bytes(&bytes)?;
641 assert_eq!(pubk.keynum, pubk2.keynum);
642
643 assert!(matches!(
645 PubKey::from_bytes(&bytes[..41]),
646 Err(Error::InvalidKeyLength)
647 ));
648 let mut long = bytes.clone();
649 long.push(0);
650 assert!(matches!(
651 PubKey::from_bytes(&long),
652 Err(Error::InvalidKeyLength)
653 ));
654
655 let mut bad_alg = bytes.clone();
657 bad_alg[0] = b'X';
658 assert!(matches!(
659 PubKey::from_bytes(&bad_alg),
660 Err(Error::UnsupportedPkAlgo)
661 ));
662
663 Ok(())
664 }
665
666 #[test]
667 fn test_sig_serialization() -> crate::error::Result<()> {
668 let sig = Sig {
669 pkalg: PKALG,
670 keynum: [1u8; KEYNUMLEN],
671 sig: [0u8; 64],
672 };
673 let bytes = sig.to_bytes();
674 let sig2 = Sig::from_bytes(&bytes)?;
675 assert_eq!(sig.keynum, sig2.keynum);
676
677 assert!(matches!(
679 Sig::from_bytes(&bytes[..73]),
680 Err(Error::InvalidKeyLength)
681 ));
682
683 let mut long = bytes.clone();
684 long.push(0);
685 assert!(matches!(
686 Sig::from_bytes(&long),
687 Err(Error::InvalidKeyLength)
688 ));
689
690 let mut bad_alg = bytes.clone();
692 bad_alg[0] = b'X';
693 assert!(matches!(
694 Sig::from_bytes(&bad_alg),
695 Err(Error::UnsupportedPkAlgo)
696 ));
697
698 Ok(())
699 }
700
701 #[test]
702 #[cfg_attr(any(target_arch = "wasm32", target_arch = "wasm64"), ignore)]
703 fn test_file_io() -> std::result::Result<(), Box<dyn std::error::Error>> {
704 let dir = tempfile::tempdir()?;
705 let path = dir.path().join("secret.key");
706 let data = b"secret data";
707
708 write(&path, b"mycomment", data)?;
709
710 let (read_data, comment) = parse::<Vec<u8>, _>(&path, |b| Ok(b.to_vec()))?;
711 assert_eq!(read_data, data);
712 assert_eq!(comment, b"mycomment");
713
714 let missing = dir.path().join("missing");
716 let result = parse::<Vec<u8>, _>(&missing, |_| Ok(vec![]));
717 #[cfg(not(target_os = "linux"))]
718 assert!(matches!(result, Err(Error::Io(_))));
719 #[cfg(target_os = "linux")]
720 assert!(matches!(result, Err(Error::Nix(_))));
721
722 let bad_prefix = dir.path().join("bad_prefix");
724 let mut f = OpenOptions::new()
725 .write(true)
726 .create_new(true)
727 .open(&bad_prefix)?;
728 f.write_all(b"invalid header\n")?;
729 assert!(matches!(
730 parse::<Vec<u8>, _>(&bad_prefix, |_| Ok(vec![])),
731 Err(Error::InvalidCommentHeader)
732 ));
733
734 let no_newline = dir.path().join("no_newline");
736 let mut f = OpenOptions::new()
737 .write(true)
738 .create_new(true)
739 .open(&no_newline)?;
740 f.write_all(b"untrusted comment: foo")?;
741 assert!(matches!(
742 parse::<Vec<u8>, _>(&no_newline, |_| Ok(vec![])),
743 Err(Error::InvalidCommentHeader)
744 ));
745
746 let bad_utf8 = dir.path().join("bad_utf8");
750 write(&bad_utf8, b"comment", b"")?;
751 let mut f = OpenOptions::new().write(true).open(&bad_utf8)?;
752 f.write_all(b"untrusted comment: comment\n\xFF\xFF\n")?;
753 assert!(matches!(
754 parse::<Vec<u8>, _>(&bad_utf8, |_| Ok(vec![])),
755 Err(Error::InvalidSignatureUtf8)
756 ));
757
758 Ok(())
759 }
760
761 #[test]
762 #[cfg(unix)]
763 fn test_open_symlink_fail() -> std::result::Result<(), Box<dyn std::error::Error>> {
764 use std::os::unix::fs::symlink;
765 let dir = tempfile::tempdir()?;
766 let target = dir.path().join("target");
767 let link = dir.path().join("link");
768
769 std::fs::write(&target, b"target")?;
770 symlink(&target, &link)?;
771
772 assert!(open(&link, false).is_err());
774
775 Ok(())
776 }
777
778 #[test]
779 #[cfg(unix)]
780 fn test_open_write_mode() -> std::result::Result<(), Box<dyn std::error::Error>> {
781 use std::os::unix::fs::PermissionsExt;
782
783 let dir = tempfile::tempdir()?;
784 let path = dir.path().join("secret.key");
785
786 let _f = open(&path, true)?;
787
788 let metadata = std::fs::metadata(&path)?;
789 let mode = metadata.permissions().mode();
790
791 assert_eq!(mode & 0o777, 0o600);
793
794 Ok(())
795 }
796
797 #[test]
798 fn test_open_parent_dir_fail() {
799 let path = Path::new("foo/../bar");
800 assert!(matches!(open(path, false), Err(Error::InvalidPath)));
801 assert!(matches!(open(path, true), Err(Error::InvalidPath)));
802 }
803
804 #[test]
805 #[cfg(target_os = "linux")]
806 fn test_safe_open_not_file_fail() -> std::result::Result<(), Box<dyn std::error::Error>> {
807 let dir = tempfile::tempdir()?;
808 let path = dir.path();
809
810 assert!(matches!(open(path, false), Err(Error::InvalidPath)));
812
813 Ok(())
814 }
815
816 #[test]
817 #[cfg(target_os = "linux")]
818 fn test_open_magiclink_fail() {
819 let path = Path::new("/proc/self/root");
820 assert!(matches!(
821 open(path, false),
822 Err(Error::Nix(nix::errno::Errno::ELOOP))
823 ));
824 }
825
826 #[test]
827 #[cfg(target_os = "linux")]
828 fn test_open_char_device_fail() {
829 let path = Path::new("/dev/null");
830 assert!(matches!(open(path, false), Err(Error::InvalidPath)));
831 }
832
833 #[test]
834 #[cfg(target_os = "linux")]
835 fn test_open_fifo_fail() -> std::result::Result<(), Box<dyn std::error::Error>> {
836 use nix::sys::stat::Mode;
837 use nix::unistd::mkfifo;
838
839 let dir = tempfile::tempdir()?;
840 let path = dir.path().join("test.fifo");
841
842 mkfifo(&path, Mode::S_IRUSR | Mode::S_IWUSR)?;
843
844 assert!(matches!(open(&path, false), Err(Error::InvalidPath)));
846
847 Ok(())
848 }
849
850 #[test]
851 #[cfg(target_os = "linux")]
852 fn test_open_socket_fail() -> std::result::Result<(), Box<dyn std::error::Error>> {
853 use std::os::unix::net::UnixListener;
854
855 let dir = tempfile::tempdir()?;
856 let path = dir.path().join("test.sock");
857
858 let _listener = UnixListener::bind(&path)?;
859
860 assert!(matches!(open(&path, false), Err(Error::InvalidPath)));
862
863 Ok(())
864 }
865
866 #[test]
867 #[cfg(unix)]
868 fn test_check_name_valid() {
869 let valid_filenames = [
870 "valid_filename.txt",
871 "hello_world",
872 "File123",
873 "Makefile",
874 "こんにちは", "文件", "emoji😀", "valid~name", "name~", "a",
880 "normal",
881 "test-file",
882 "test_file",
883 "file name",
884 "file☃name", "name\u{0080}", "name\u{00FE}", "😀name", "name😀", "😀", "name😀name", "na~me", "name-", "name_", "name.", "a\u{0020}b", "a\u{00A0}b", "a\u{1680}b", "a\u{2007}b", "a\u{202F}b", "a\u{3000}b", ];
902
903 for (idx, name) in valid_filenames.iter().enumerate() {
904 assert!(
905 safe_name(Path::new(name)).is_ok(),
906 "Filename {idx} '{name}' should be valid"
907 );
908 }
909 }
910
911 #[test]
912 #[cfg(unix)]
913 fn test_check_name_invalid() {
914 let invalid_filenames: &[&[u8]] = &[
915 b"", b"-", b"*", b"?", b"!", b"$", b"`", b" -", b"~home", b"*home", b"?home", b"!home", b"$home", b"`home", b"file ", b"file*", b"file?", b"file!", b"file$", b"file`", b"bad*name", b"bad?name", b"bad!name", b"bad$name", b"bad`name", b"bad\nname", b"\0", b"bad\0name", b"bad\x7Fname", b"bad\xFFname", b"\x1Fcontrol", b"name\x1F", b"name\x7F", b"name\xFF", b"name ", b"-name", b" name", b"~name", b"*name", b"?name", b"!name", b"$name", b"`name", b"name\x19", b"name\n", b"\nname", b"na\nme", b"name\t", b"name\r", b"name\x1B", b"name\x00", b"name\x7F", b"name\xFF", b"\xFF", b"name\x80\xFF", b"name\xC0\xAF", b"\xF0\x28\x8C\xBC", b"\xF0\x90\x28\xBC", b"\xF0\x28\x8C\x28", b"name\xFFname", b"name\xC3\x28", b"name\xA0\xA1", b"\xE2\x28\xA1", b"\xE2\x82\x28", b"\xF0\x28\x8C\xBC", b"\xF0\x90\x28\xBC", b"\xF0\x28\x8C\x28", b"\xC2\xA0", b"\x20file", b"file\x20", b"\xC2\xA0file", b"file\xE3\x80\x80", b"\xE2\x80\xAFfile", b"\xE2\x81\x9Ffile\xE2\x81\x9F", ];
990
991 for (idx, name) in invalid_filenames.iter().enumerate() {
992 assert!(
993 safe_name(Path::new(OsStr::from_bytes(name))).is_err(),
994 "Filename {idx} '{name:?}' should not be valid"
995 );
996 }
997 }
998
999 #[test]
1000 #[cfg(unix)]
1001 fn test_check_name_control_characters() {
1002 for b in 0x00..=0x1F {
1003 if let Some(c) = char::from_u32(b as u32) {
1004 let name = format!("name{c}char");
1005 assert!(
1006 safe_name(Path::new(&name)).is_err(),
1007 "Filename with control character '\\x{b:02X}' should be invalid",
1008 );
1009 }
1010 }
1011 }
1012
1013 #[test]
1014 #[cfg(unix)]
1015 fn test_check_name_extended_ascii_characters() {
1016 for b in 0x80..=0xFE {
1017 if b == 0xFF {
1018 continue; }
1020 let mut bytes = b"name".to_vec();
1021 bytes.push(b);
1022 bytes.extend_from_slice(b"char");
1023 let name = OsStr::from_bytes(&bytes);
1024 let name = Path::new(name);
1025 let result = safe_name(name);
1026 if std::str::from_utf8(&bytes).is_ok() {
1027 assert!(result.is_ok(), "Filename with byte 0x{b:X} should be valid",);
1028 } else {
1029 assert!(
1030 result.is_err(),
1031 "Filename with invalid UTF-8 byte 0x{b:X} should be invalid",
1032 );
1033 }
1034 }
1035 }
1036
1037 #[test]
1038 #[cfg(unix)]
1039 fn test_check_name_edge_cases() {
1040 let valid_single_chars = [
1042 "a", "b", "Z", "9", "_", "😀", ];
1044
1045 for (idx, name) in valid_single_chars.iter().enumerate() {
1046 assert!(
1047 safe_name(Path::new(name)).is_ok(),
1048 "Single-character filename {idx} '{name}' should be valid",
1049 );
1050 }
1051
1052 let invalid_single_chars: &[&[u8]] = &[
1053 b".", b"-", b" ", b"~", b"*", b"?", b"\n", b"\r", b"\x7F", b"\x1F", b"\xFF", b"\0", b"\xC2\xA0", b"\x20", b"\xC2\xA0", b"\xE1\x9A\x80", b"\xE2\x80\x87", b"\xE2\x80\xAF", b"\xE3\x80\x80", ];
1073
1074 for (idx, name) in invalid_single_chars.iter().enumerate() {
1075 assert!(
1076 safe_name(Path::new(OsStr::from_bytes(name))).is_err(),
1077 "Single-character filename {idx} '{name:?}' should be invalid",
1078 );
1079 }
1080 }
1081}