- 1
//! The workspace inbox (docs/design/72, "File in"): where a file a person - 2
//! sends, on a channel or dropped in a client, is saved and named. Its bytes - 3
//! never enter a prompt unless they are text small enough to inline; the - 4
//! message carries `note`, which tells the model where the file is and - 5
//! which reader understands it. - 6
- 7
pub(crate) use vak_tools::INBOX_DIR; - 8
- 9
/// The largest file a client may drop in. An Office package may inflate to - 10
/// four times this within the reader's own bound. - 11
pub(crate) const UPLOAD_MAX_BYTES: usize = 64 * 1024 * 1024; - 12
- 13
/// The line that names a saved file to the model in place of its bytes. - 14
pub(crate) fn note(filename: &str, saved: &str, bytes: &[u8]) -> String { - 15
let size = format!("{} KiB", bytes.len().div_ceil(1024)); - 16
let text = std::str::from_utf8(bytes) - 17
.ok() - 18
.filter(|text| !text.contains('\0')) - 19
.is_some(); - 20
// The path leads and is quoted as the argument to pass: a small model - 21
// given the sent name first called the reader with that name instead. - 22
if vak_ooxml::is_openxml_path(saved) { - 23
format!( - 24
"[attached file at path \"{saved}\" ({size}, sent as '{filename}'). Read it with doc_read and that exact path; its contents are not in this message.]" - 25
) - 26
} else if text { - 27
format!( - 28
"[attached text file at path \"{saved}\" ({size}, sent as '{filename}'). Read it with read or doc_read and that exact path; its contents are not in this message.]" - 29
) - 30
} else { - 31
format!( - 32
"[attached file at path \"{saved}\" ({size}, sent as '{filename}'). It is not a text or Open XML file, so no reader here understands it yet; its bytes are not in this message.]" - 33
) - 34
} - 35
} - 36
- 37
/// Saves received bytes under `<workspace>/inbox/` with a digest-prefixed, - 38
/// sanitised name. Returns the workspace-relative path. Never overwrites, - 39
/// never follows a planted symlink out of the workspace, and saving the - 40
/// same bytes under the same name twice yields the same file. - 41
pub(crate) fn save_to_inbox( - 42
workspace: &std::path::Path, - 43
filename: &str, - 44
bytes: &[u8], - 45
) -> Result<String, String> { - 46
use sha2::Digest as _; - 47
use std::io::Write as _; - 48
let workspace = workspace - 49
.canonicalize() - 50
.map_err(|error| format!("workspace unavailable: {error}"))?; - 51
let inbox = workspace.join(INBOX_DIR); - 52
std::fs::create_dir_all(&inbox).map_err(|error| format!("inbox: {error}"))?; - 53
let inbox = inbox - 54
.canonicalize() - 55
.map_err(|error| format!("inbox: {error}"))?; - 56
if !inbox.starts_with(&workspace) { - 57
return Err("inbox resolves outside the workspace".into()); - 58
} - 59
let digest = sha2::Sha256::digest(bytes); - 60
let prefix: String = digest - 61
.iter() - 62
.take(6) - 63
.map(|byte| format!("{byte:02x}")) - 64
.collect(); - 65
let name = format!("{prefix}-{}", sanitize_filename(filename)); - 66
let path = inbox.join(&name); - 67
let relative = format!("{INBOX_DIR}/{name}"); - 68
match std::fs::OpenOptions::new() - 69
.write(true) - 70
.create_new(true) - 71
.open(&path) - 72
{ - 73
Ok(mut file) => { - 74
file.write_all(bytes) - 75
.and_then(|()| file.sync_all()) - 76
.map_err(|error| format!("write failed: {error}"))?; - 77
Ok(relative) - 78
} - 79
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { - 80
let existing = - 81
std::fs::symlink_metadata(&path).map_err(|error| format!("inbox: {error}"))?; - 82
if existing.file_type().is_file() && std::fs::read(&path).ok().as_deref() == Some(bytes) - 83
{ - 84
Ok(relative) - 85
} else { - 86
Err("a different file already has that name".into()) - 87
} - 88
} - 89
Err(error) => Err(format!("create failed: {error}")), - 90
} - 91
} - 92
- 93
/// The last path component, with anything outside a conservative - 94
/// character set replaced, no leading dots, and a bounded length. - 95
pub(crate) fn sanitize_filename(filename: &str) -> String { - 96
let last = filename.rsplit(['/', '\\']).next().unwrap_or_default(); - 97
let cleaned: String = last - 98
.chars() - 99
.map(|character| { - 100
if character.is_alphanumeric() || matches!(character, '.' | '-' | '_' | ' ') { - 101
character - 102
} else { - 103
'_' - 104
} - 105
}) - 106
.collect(); - 107
let cleaned = cleaned.trim().trim_start_matches('.').trim(); - 108
let mut name: String = cleaned - 109
.chars() - 110
.rev() - 111
.take(120) - 112
.collect::<Vec<_>>() - 113
.into_iter() - 114
.rev() - 115
.collect(); - 116
if name.is_empty() { - 117
name = "file".into(); - 118
} - 119
name - 120
} - 121
- 122
/// A file already saved in `workspace`'s inbox, named by the path - 123
/// `save_to_inbox` returned: its note for the model and what a client shows. - 124
/// Refuses anything that is not a regular file directly inside the inbox. - 125
pub(crate) fn attached( - 126
workspace: &std::path::Path, - 127
relative: &str, - 128
) -> Result<(String, vak_session::AttachedFile), String> { - 129
let saved_name = relative - 130
.strip_prefix(INBOX_DIR) - 131
.and_then(|rest| rest.strip_prefix('/')) - 132
.filter(|name| !name.is_empty() && !name.contains(['/', '\\']) && *name != "..") - 133
.ok_or_else(|| format!("{relative} is not a file in {INBOX_DIR}/"))?; - 134
let workspace = workspace - 135
.canonicalize() - 136
.map_err(|error| format!("workspace unavailable: {error}"))?; - 137
let inbox = workspace - 138
.join(INBOX_DIR) - 139
.canonicalize() - 140
.map_err(|_| format!("{relative} was not found"))?; - 141
let path = inbox.join(saved_name); - 142
let metadata = - 143
std::fs::symlink_metadata(&path).map_err(|_| format!("{relative} was not found"))?; - 144
if !inbox.starts_with(&workspace) || !metadata.file_type().is_file() { - 145
return Err(format!("{relative} is not a file in {INBOX_DIR}/")); - 146
} - 147
let bytes = std::fs::read(&path).map_err(|error| format!("{relative}: {error}"))?; - 148
let name = display_name(saved_name); - 149
let relative = format!("{INBOX_DIR}/{saved_name}"); - 150
Ok(( - 151
note(&name, &relative, &bytes), - 152
vak_session::AttachedFile { - 153
block: 0, - 154
path: relative, - 155
name, - 156
bytes: bytes.len() as u64, - 157
}, - 158
)) - 159
} - 160
- 161
/// The name a saved file had before `save_to_inbox` prefixed its digest. - 162
pub(crate) fn display_name(saved_name: &str) -> String { - 163
match saved_name.split_once('-') { - 164
Some((prefix, rest)) - 165
if prefix.len() == 12 && prefix.chars().all(|c| c.is_ascii_hexdigit()) => - 166
{ - 167
rest.to_string() - 168
} - 169
_ => saved_name.to_string(), - 170
} - 171
} - 172
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.