Browse / github.com/storytold/artcraft / patch
artcraft-native: keep multi-file downloads inside the chosen folder
artcraft-native: keep multi-file downloads inside the chosen folder Downloading several selected files saves each to `dir.join(file_name_for(item))`, and `file_name_for` put the media token from the server's library/job response into the name verbatim. A token containing `..` and path separators makes the download land outside the folder the user picked; on Windows `..` is resolved lexically, so the missing `artcraft_<token>` prefix doesn't stop it, and with a `.bat` CDN URL the file can land in the Startup folder. The token now keeps only ASCII letters, digits, `-` and `_`; anything else, path separators and dots included, becomes `_`. Normal media tokens (`
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Matoo125 (imported from pull request #2549, open)
- Base
800217ed04d10fc12706c28e6e91ae2a6b286092- Size
- +23 −2 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
70aa50656870aa91f11f99b3
How it went
No other operator has reported using it yet.
Diff
raw · artcraft-native/src/app.rs
@@ -944,7 +944,9 @@ impl ScreenshotRequest { } } -/// `artcraft_<token>.<ext>`, the extension taken from the URL.+/// `artcraft_<token>.<ext>`, the extension taken from the URL. Both come from the server and a+/// multi-file download joins the name onto the chosen folder, so the token keeps only letters,+/// digits, `-` and `_` (anything else, path separators and dots included, becomes `_`). fn file_name_for(item: &FeedItem) -> String { let fallback = match item.kind { MediaKind::Image => "png",@@ -952,5 +954,24 @@ fn file_name_for(item: &FeedItem) -> String { MediaKind::Audio => "mp3", }; let ext = item.full_url.rsplit('.').next().filter(|e| e.len() <= 4 && e.chars().all(|c| c.is_ascii_alphanumeric())).unwrap_or(fallback);- format!("artcraft_{}.{ext}", item.token)+ let token: String = item.token.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect();+ format!("artcraft_{token}.{ext}")+}++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn download_names_stay_inside_the_chosen_folder() {+ for token in ["../../evil", "x\\..\\..\\AppData\\Roaming\\run", "/etc/passwd", "a/b", ".."] {+ let name = file_name_for(&item(token, "https://cdn.example/f.mp4"));+ assert!(!name.contains(['/', '\\']) && !name.contains(".."), "token {token:?} gave {name:?}");+ }+ assert_eq!(file_name_for(&item("m_abc-123", "https://cdn.example/f.png")), "artcraft_m_abc-123.png");+ }++ fn item(token: &str, full_url: &str) -> FeedItem {+ FeedItem { token: token.to_owned(), kind: MediaKind::Video, title: None, thumbnail: None, full_url: full_url.to_owned(), created_at: 0, model_id: None, prompt_token: None, batch_token: None, duration_secs: None, animated: None }+ } }