Compare commits

..

7 commits

12 changed files with 183 additions and 128 deletions

View file

@ -32,7 +32,8 @@ impl AsRef<[u8]> for Checked {
} }
impl Checked { impl Checked {
pub(super) fn new_direct(path: PathBuf, size: u64, hash: Option<String>) -> Self { /// create this directly, without any checks
pub(super) unsafe fn new_unchecked(path: PathBuf, size: u64, hash: Option<String>) -> Self {
Self { path, size, hash } Self { path, size, hash }
} }
@ -93,9 +94,7 @@ impl Checked {
) -> crate::Result<Uploading> { ) -> crate::Result<Uploading> {
let file_id = client.file_create(uri, alias_id, share_id, &self)?; let file_id = client.file_create(uri, alias_id, share_id, &self)?;
Ok(Uploading::new_direct( Ok(unsafe { Uploading::new_unchecked(self.path, self.size, self.hash, file_id) })
self.path, self.size, self.hash, file_id,
))
} }
} }
@ -117,16 +116,19 @@ impl FileTrait for Checked {
mod tests { mod tests {
use tempfile::{NamedTempFile, TempDir}; use tempfile::{NamedTempFile, TempDir};
use crate::test_util::{ use crate::{
MockClient, check_trait, create_file, sharry::{Client, json::NewShareRequest},
data::{HASHES_STD_GOOD, cases, data}, test_util::{
MockClient, check_trait, create_file,
data::{HASHES_STD_GOOD, cases, data},
},
}; };
use super::*; use super::*;
fn create_checked(content: &[u8]) -> (Checked, NamedTempFile) { fn create_checked(content: &[u8]) -> (Checked, NamedTempFile) {
let file = create_file(content); let file = create_file(content);
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed"); let chk = Checked::new(file.path()).unwrap();
// return both, so the `NamedTempFile` is not auto-deleted here // return both, so the `NamedTempFile` is not auto-deleted here
(chk, file) (chk, file)
@ -136,21 +138,14 @@ mod tests {
fn new_on_existing_file_works() { fn new_on_existing_file_works() {
for (content, size) in cases() { for (content, size) in cases() {
let (chk, file) = create_checked(content); let (chk, file) = create_checked(content);
let path = file.path().canonicalize().unwrap();
let path = file
.path()
.canonicalize()
.expect("the file should have a canonical path");
assert_eq!(chk.path, path); assert_eq!(chk.path, path);
assert_eq!(chk.size, size); assert_eq!(chk.size, size);
assert!(chk.hash.is_none()); assert!(chk.hash.is_none());
// `FileTrait` // `FileTrait`
assert_eq!( assert_eq!(chk.get_name(), file.path().file_name().unwrap());
chk.get_name(),
file.path().file_name().expect("`file_name` should succeed")
);
assert_eq!(chk.get_size(), size); assert_eq!(chk.get_size(), size);
check_trait( check_trait(
@ -160,8 +155,8 @@ mod tests {
"Checked", "Checked",
); );
// new_direct // new_unchecked
let chk = Checked::new_direct(chk.path, chk.size, chk.hash); let chk = unsafe { Checked::new_unchecked(chk.path, chk.size, chk.hash) };
assert_eq!(chk.path, path); assert_eq!(chk.path, path);
assert_eq!(chk.size, size); assert_eq!(chk.size, size);
assert!(chk.hash.is_none()); assert!(chk.hash.is_none());
@ -170,13 +165,13 @@ mod tests {
#[test] #[test]
fn new_on_dir_errors() { fn new_on_dir_errors() {
let tempdir = TempDir::new().expect("creating temp dir"); let tempdir = TempDir::new().unwrap();
let fs_root = PathBuf::from("/"); let fs_root = PathBuf::from("/");
let dirs = [tempdir.path(), fs_root.as_path()]; let dirs = [tempdir.path(), fs_root.as_path()];
for p in dirs { for p in dirs {
let err = Checked::new(p).expect_err("creating `Checked` should fail"); let err = Checked::new(p).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput); assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
assert_eq!(err.to_string(), "Not a regular file"); assert_eq!(err.to_string(), "Not a regular file");
@ -185,11 +180,11 @@ mod tests {
#[test] #[test]
fn new_on_nex_errors() { fn new_on_nex_errors() {
let tempdir = TempDir::new().expect("creating temp dir"); let tempdir = TempDir::new().unwrap();
let nex_paths = [0, 1, 2, 3, 4].map(|i| tempdir.path().join(format!("nex_{i}.ext"))); let nex_paths = [0, 1, 2, 3, 4].map(|i| tempdir.path().join(format!("nex_{i}.ext")));
for p in nex_paths { for p in nex_paths {
let err = Checked::new(p).expect_err("creating `Checked` should fail"); let err = Checked::new(p).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound); assert_eq!(err.kind(), io::ErrorKind::NotFound);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
assert_eq!(err.to_string(), "No such file or directory (os error 2)"); assert_eq!(err.to_string(), "No such file or directory (os error 2)");
@ -201,9 +196,9 @@ mod tests {
for (content, hash) in data().zip(HASHES_STD_GOOD) { for (content, hash) in data().zip(HASHES_STD_GOOD) {
let (mut chk, _file) = create_checked(content); let (mut chk, _file) = create_checked(content);
chk.hash(drop).expect("`hash` should succeed"); chk.hash(drop).unwrap();
// `FileTrait` // `FileTrait`
chk.check_hash(drop).expect("`check_hash` should succeed"); chk.check_hash(drop).unwrap();
assert_eq!(chk.hash, Some(hash.to_string())); assert_eq!(chk.hash, Some(hash.to_string()));
} }
@ -216,7 +211,7 @@ mod tests {
// fake hash // fake hash
chk.hash = Some(String::default()); chk.hash = Some(String::default());
let err = chk.hash(drop).expect_err("`hash` twice should fail"); let err = chk.hash(drop).unwrap_err();
assert!(err.is_mismatch("unhashed file", chk.path.display().to_string())); assert!(err.is_mismatch("unhashed file", chk.path.display().to_string()));
} }
@ -225,19 +220,19 @@ mod tests {
#[test] #[test]
fn start_upload_works() { fn start_upload_works() {
let client = MockClient::default(); let client = MockClient::default();
let share_id = client.add_share(); let uri = sharry::Uri::from(true);
let alias_id = sharry::AliasID::from(true);
let share_id = client
.share_create(&uri, &alias_id, NewShareRequest::new("share", 0))
.unwrap();
for content in data() { for content in data() {
let (chk, _file) = create_checked(content); let (chk, _file) = create_checked(content);
assert!( assert!(
chk.start_upload( chk.start_upload(&client, &uri, &alias_id, &share_id)
&client, .is_ok()
&sharry::Uri::from(true),
&sharry::AliasID::from(true),
&share_id
)
.is_ok()
); );
} }
} }

View file

@ -40,7 +40,12 @@ where
} }
impl<'t> Chunk<'t> { impl<'t> Chunk<'t> {
pub(super) fn new_direct(file_id: sharry::FileID, offset: u64, data: &'t [u8]) -> Self { /// create this directly, without any checks
pub(super) unsafe fn new_unchecked(
file_id: sharry::FileID,
offset: u64,
data: &'t [u8],
) -> Self {
Self { Self {
file_id, file_id,
offset, offset,
@ -83,9 +88,10 @@ mod tests {
#[test] #[test]
fn basic_tests() { fn basic_tests() {
// items from `DATA_LENGTHS_BAD` used as mock offsets
for (data, len, mock_offset) in cases_with(DATA_LENGTHS_BAD) { for (data, len, mock_offset) in cases_with(DATA_LENGTHS_BAD) {
let fid = sharry::FileID::default(); let fid = sharry::FileID::default();
let chunk = Chunk::new_direct(fid, mock_offset, data); let chunk = unsafe { Chunk::new_unchecked(fid, mock_offset, data) };
let repr_expect = format!( let repr_expect = format!(
"Chunk {{ file_id: {:?}, offset: {:?}, data.len(): {:?}, .. }}", "Chunk {{ file_id: {:?}, offset: {:?}, data.len(): {:?}, .. }}",

View file

@ -139,8 +139,7 @@ mod tests {
// to capture progress updates from `compute_hash` // to capture progress updates from `compute_hash`
let mut read_total = 0; let mut read_total = 0;
let callback = |n| read_total += n; let callback = |n| read_total += n;
let hash = compute_hash(file.path(), size, callback).unwrap();
let hash = compute_hash(file.path(), size, callback).expect("hash should succeed");
assert_eq!(hash, expected_hash); assert_eq!(hash, expected_hash);
assert_eq!(read_total, size); assert_eq!(read_total, size);
@ -151,21 +150,16 @@ mod tests {
fn hash_size_mismatch() { fn hash_size_mismatch() {
for (content, good_size, bad_size) in cases_with(DATA_LENGTHS_BAD) { for (content, good_size, bad_size) in cases_with(DATA_LENGTHS_BAD) {
let file = create_file(content); let file = create_file(content);
let callback = drop;
{ {
let err = compute_hash(file.path(), bad_size, callback) // `compute_hash` with bad size
.expect_err("compute_hash should report a mismatch"); let err = compute_hash(file.path(), bad_size, drop).unwrap_err();
// check error
assert!(err.is_mismatch(bad_size.to_string(), good_size.to_string())); assert!(err.is_mismatch(bad_size.to_string(), good_size.to_string()));
} }
{ {
let err = check_hash(file.path(), bad_size, Some("foobar"), callback) // `check_hash` with bad size
.expect_err("check_hash should report a mismatch"); let err = check_hash(file.path(), bad_size, Some("foobar"), drop).unwrap_err();
// check error
assert!(err.is_mismatch(bad_size.to_string(), good_size.to_string())); assert!(err.is_mismatch(bad_size.to_string(), good_size.to_string()));
} }
} }
@ -175,12 +169,9 @@ mod tests {
fn hash_value_none() { fn hash_value_none() {
for (content, size) in cases() { for (content, size) in cases() {
let file = create_file(content); let file = create_file(content);
let callback = drop;
let err = check_hash(file.path(), size, None, callback) // `check_hash` with no hash
.expect_err("check_hash should report a mismatch"); let err = check_hash(file.path(), size, None, drop).unwrap_err();
// check error
assert!(err.is_mismatch("hash", file.path().display().to_string())); assert!(err.is_mismatch("hash", file.path().display().to_string()));
} }
} }
@ -191,12 +182,9 @@ mod tests {
cases_with(HASHES_STD_GOOD).zip(HASHES_STD_BAD) cases_with(HASHES_STD_GOOD).zip(HASHES_STD_BAD)
{ {
let file = create_file(content); let file = create_file(content);
let callback = drop;
let err = check_hash(file.path(), size, Some(bad_hash), callback) // `check_hash` with bad hash
.expect_err("check_hash should report a mismatch"); let err = check_hash(file.path(), size, Some(bad_hash), drop).unwrap_err();
// check error
assert!(err.is_mismatch(bad_hash, good_hash)); assert!(err.is_mismatch(bad_hash, good_hash));
} }
} }

View file

@ -14,7 +14,6 @@ use super::{Checked, Chunk, FileTrait};
/// Description of a `file::Checked` that is actively being uploaded /// Description of a `file::Checked` that is actively being uploaded
/// ///
/// - impl `serde` for cachefile handling /// - impl `serde` for cachefile handling
/// - impl `Into<Checked>` to abort an upload
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Uploading { pub struct Uploading {
/// canonical path to a regular file /// canonical path to a regular file
@ -33,7 +32,8 @@ pub struct Uploading {
} }
impl Uploading { impl Uploading {
pub(super) fn new_direct( /// create this directly, without any checks
pub(super) unsafe fn new_unchecked(
path: PathBuf, path: PathBuf,
size: u64, size: u64,
hash: Option<String>, hash: Option<String>,
@ -93,7 +93,8 @@ impl Uploading {
)); ));
} }
let chunk = Chunk::new_direct(self.file_id.clone(), self.offset, &buf[..read_len]); let chunk =
unsafe { Chunk::new_unchecked(self.file_id.clone(), self.offset, &buf[..read_len]) };
self.previous_offset = Some(self.offset); self.previous_offset = Some(self.offset);
self.offset += chunk.get_length(); self.offset += chunk.get_length();
@ -117,7 +118,7 @@ impl Uploading {
/// ///
/// - consume self, returning as a `file::Checked` /// - consume self, returning as a `file::Checked`
pub fn stop(self) -> Checked { pub fn stop(self) -> Checked {
Checked::new_direct(self.path, self.size, self.hash) unsafe { Checked::new_unchecked(self.path, self.size, self.hash) }
} }
} }
@ -134,3 +135,82 @@ impl FileTrait for Uploading {
super::check_hash(&self.path, self.size, self.hash.as_deref(), on_progress) super::check_hash(&self.path, self.size, self.hash.as_deref(), on_progress)
} }
} }
#[cfg(test)]
mod tests {
use tempfile::NamedTempFile;
use crate::{
sharry::{Client, json::NewShareRequest},
test_util::{MockClient, create_file, data::cases},
};
use super::*;
fn create_uploading(content: &[u8]) -> (Uploading, sharry::ShareID, NamedTempFile) {
let client = MockClient::default();
let uri = sharry::Uri::from(true);
let alias_id = sharry::AliasID::from(true);
let share_id = client
.share_create(&uri, &alias_id, NewShareRequest::new("share", 0))
.expect("");
let file = create_file(content);
let upl = Checked::new(file.path())
.unwrap()
.start_upload(&client, &uri, &alias_id, &share_id)
.unwrap();
// return all, so the `NamedTempFile` is not auto-deleted here
(upl, share_id, file)
}
#[test]
fn basic_tests() {
fn check_members(upl: &Uploading, path: &PathBuf, size: u64) {
assert_eq!(&upl.path, path);
assert_eq!(upl.size, size);
assert_eq!(upl.offset, 0);
assert!(upl.previous_offset.is_none());
assert!(upl.hash.is_none());
}
for (content, size) in cases() {
let (upl, _share_id, file) = create_uploading(content);
let path = file.path().canonicalize().unwrap();
check_members(&upl, &path, size);
// `get_offset`
assert_eq!(upl.get_offset(), upl.offset);
// `FileTrait`
assert_eq!(upl.get_name(), file.path().file_name().unwrap());
assert_eq!(upl.get_size(), size);
assert!(upl.check_hash(drop).is_err());
// `new_unchecked`
let upl =
unsafe { Uploading::new_unchecked(upl.path, upl.size, upl.hash, upl.file_id) };
check_members(&upl, &path, size);
// TODO into separate test
// // `check_eof`
// let upl = if size == 0 {
// upl
// } else {
// let eof = upl.check_eof();
// assert!(eof.is_ok());
// let upl = eof.unwrap();
// check_members(&upl, &path, size);
// upl
// };
// `stop`
let chk = upl.stop();
assert_eq!(chk.get_name(), path.file_name().unwrap().to_str().unwrap());
assert_eq!(chk.get_size(), size);
}
}
}

View file

@ -1,15 +1,16 @@
// TODO fix with documentation // TODO fix with documentation
#![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
mod appstate; mod appstate;
mod cachefile; mod cachefile;
mod cli; mod cli;
mod error; mod error;
mod file; mod file;
mod impl_ureq;
pub mod output; pub mod output;
mod sharry; mod sharry;
mod test_util; mod test_util;
mod ureq_client;
pub use appstate::AppState; pub use appstate::AppState;
pub use cli::Cli; pub use cli::Cli;

View file

@ -7,7 +7,7 @@ use log::{info, warn};
type StaticStyled<'t> = LazyLock<StyledObject<&'t str>>; type StaticStyled<'t> = LazyLock<StyledObject<&'t str>>;
pub static SHRUPL: StaticStyled = LazyLock::new(|| style("ShrUpl").yellow().bold()); pub const SHRUPL: StaticStyled = LazyLock::new(|| style("ShrUpl").yellow().bold());
#[must_use] #[must_use]
pub fn prompt_continue() -> bool { pub fn prompt_continue() -> bool {
@ -61,7 +61,6 @@ where
} }
#[must_use] #[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn new_progressbar() -> ProgressBar { pub fn new_progressbar() -> ProgressBar {
ProgressBar::no_length().with_style( ProgressBar::no_length().with_style(
ProgressStyle::with_template(&format!( ProgressStyle::with_template(&format!(

View file

@ -71,27 +71,25 @@ impl fmt::Display for FileID {
} }
} }
/// Pattern breakdown:
/// - `^([^:/?#]+)://` - scheme (anything but `:/?#`) + `"://"`
/// - `([^/?#]+)` - authority/host (anything but `/?#`)
/// - `/api/v2/alias/upload/` - literal path segment
/// - `([^/]+)` - capture SID (one or more non-slash chars)
/// - `/files/tus/` - literal path segment
/// - `(?P<fid>[^/]+)` - capture FID (one or more non-slash chars)
/// - `$` - end of string
const UPLOAD_URL_RE: LazyLock<Regex> = LazyLock::new(|| {
trace!("compiling UPLOAD_URL_RE");
Regex::new(r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P<fid>[^/]+)$")
.expect("Regex compilation failed")
});
impl TryFrom<String> for FileID { impl TryFrom<String> for FileID {
type Error = crate::Error; type Error = crate::Error;
fn try_from(value: String) -> crate::Result<Self> { fn try_from(value: String) -> crate::Result<Self> {
/// Pattern breakdown:
/// - `^([^:/?#]+)://` - scheme (anything but `:/?#`) + `"://"`
/// - `([^/?#]+)` - authority/host (anything but `/?#`)
/// - `/api/v2/alias/upload/` - literal path segment
/// - `([^/]+)` - capture SID (one or more non-slash chars)
/// - `/files/tus/` - literal path segment
/// - `(?P<fid>[^/]+)` - capture FID (one or more non-slash chars)
/// - `$` - end of string
static UPLOAD_URL_RE: LazyLock<Regex> = LazyLock::new(|| {
trace!("compiling UPLOAD_URL_RE");
Regex::new(
r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P<fid>[^/]+)$",
)
.expect("Regex compilation failed")
});
trace!("TryFrom {value:?}"); trace!("TryFrom {value:?}");
if let Some(fid) = UPLOAD_URL_RE if let Some(fid) = UPLOAD_URL_RE
@ -171,12 +169,8 @@ mod tests {
]; ];
for (good, expected_fid) in cases { for (good, expected_fid) in cases {
let file_id = let file_id = FileID::try_from(good.to_string()).unwrap();
FileID::try_from(good.to_string()).expect("URL should parse successfully"); assert_eq!(file_id.0, expected_fid);
assert_eq!(
file_id.0, expected_fid,
"Expected `{good}` → FileID({expected_fid}), got {file_id:?}",
);
} }
} }
@ -196,7 +190,7 @@ mod tests {
]; ];
for bad in bad_inputs { for bad in bad_inputs {
let err = FileID::try_from(bad.to_string()).expect_err("URL should not parse"); let err = FileID::try_from(bad.to_string()).unwrap_err();
// make sure it's the Mismatch variant, and that it contains the original input // make sure it's the Mismatch variant, and that it contains the original input
assert!(err.is_mismatch( assert!(err.is_mismatch(
"<proto>://<host>/api/v2/alias/upload/<share>/files/tus/<file>", "<proto>://<host>/api/v2/alias/upload/<share>/files/tus/<file>",

View file

@ -1,7 +1,7 @@
use std::{fmt, sync::LazyLock}; use std::{fmt, sync::LazyLock};
use log::{debug, trace}; use log::{debug, trace};
use regex::Regex; use regex::{Captures, Regex};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// ID of a file in a Sharry share /// ID of a file in a Sharry share
@ -26,33 +26,33 @@ impl AsRef<[u8]> for Uri {
} }
} }
fn captured(caps: &Captures, name: &str) -> String {
caps.name(name)
.unwrap_or_else(|| panic!("{name} not captured"))
.as_str()
.to_string()
}
/// Pattern breakdown:
/// - `^(?P<scheme>[^:/?#]+)://` - capture scheme (anything but `:/?#`) + `"://"`
/// - `(?P<host>[^/?#]+)` - capture authority/host (anything but `/?#`)
/// - `(/.*)?` - maybe trailing slash and some path
/// - `$` - end of string
const SHARRY_URI_RE: LazyLock<Regex> = LazyLock::new(|| {
trace!("compiling SHARRY_URI_RE");
Regex::new(r"^(?P<scheme>[^:/?#]+)://(?P<host>[^/?#]+)(/.*)?$")
.expect("Regex compilation failed")
});
fn parse_url(value: &str) -> Option<(String, String)> {
SHARRY_URI_RE
.captures(value)
.map(|caps| (captured(&caps, "scheme"), captured(&caps, "host")))
}
impl From<String> for Uri { impl From<String> for Uri {
fn from(value: String) -> Self { fn from(value: String) -> Self {
fn parse_url(value: &str) -> Option<(String, String)> {
/// Pattern breakdown:
/// - `^(?P<scheme>[^:/?#]+)://` - capture scheme (anything but `:/?#`) + `"://"`
/// - `(?P<host>[^/?#]+)` - capture authority/host (anything but `/?#`)
/// - `(/.*)?` - maybe trailing slash and some path
/// - `$` - end of string
static SHARRY_URI_RE: LazyLock<Regex> = LazyLock::new(|| {
trace!("compiling SHARRY_URI_RE");
Regex::new(r"^(?P<scheme>[^:/?#]+)://(?P<host>[^/?#]+)(/.*)?$")
.expect("Regex compilation failed")
});
SHARRY_URI_RE.captures(value).map(|caps| {
let captured = |name| {
caps.name(name)
.unwrap_or_else(|| panic!("{name} not captured"))
.as_str()
.to_string()
};
(captured("scheme"), captured("host"))
})
}
trace!("TryFrom {value:?}"); trace!("TryFrom {value:?}");
if let Some((scheme, host)) = parse_url(&value) { if let Some((scheme, host)) = parse_url(&value) {

View file

@ -71,7 +71,7 @@ impl MockClient {
.ok_or_else(|| error_response!("can't find share {share_id:?}!"))?; .ok_or_else(|| error_response!("can't find share {share_id:?}!"))?;
Ok(RefMut::map(shares, |shares| { Ok(RefMut::map(shares, |shares| {
shares.get_mut(share_id).expect("checked but None!") shares.get_mut(share_id).unwrap()
})) }))
} }
@ -90,17 +90,9 @@ impl MockClient {
.ok_or_else(|| error_response!("can't find file {file_id:?}!"))?; .ok_or_else(|| error_response!("can't find file {file_id:?}!"))?;
Ok(RefMut::map(share, move |share| { Ok(RefMut::map(share, move |share| {
share.files.get_mut(file_id).expect("checked but None!") share.files.get_mut(file_id).unwrap()
})) }))
} }
pub fn add_share(&self) -> ShareID {
let share_id = ShareID::from(true);
self.insert_share(&share_id, MockShare::default())
.expect("should never fail");
share_id
}
} }
impl Client for MockClient { impl Client for MockClient {

View file

@ -142,8 +142,8 @@ mod tests {
#[test] #[test]
fn false_makes_invalids() { fn false_makes_invalids() {
fn test_check(value: impl CheckID, callback: impl FnOnce(&Parameter) -> bool) { fn test_check(value: impl CheckID, callback: impl FnOnce(&Parameter) -> bool) {
let check = value.check().expect_err("should be invalid"); let check = value.check().unwrap_err();
let p = check.get_invalid_param().expect("should be InvalidParam"); let p = check.get_invalid_param().unwrap();
assert!(callback(p)); assert!(callback(p));
} }

View file

@ -24,7 +24,7 @@ where
/// Helper to create a temp file from `data` /// Helper to create a temp file from `data`
pub fn create_file(data: &[u8]) -> NamedTempFile { pub fn create_file(data: &[u8]) -> NamedTempFile {
let mut tmp = NamedTempFile::new().expect("creating temp file"); let mut tmp = NamedTempFile::new().unwrap();
tmp.write_all(data).expect("writing to tempfile"); tmp.write_all(data).unwrap();
tmp tmp
} }