From 1ff53b04c40850f2bf83cefb497ea7310dcd0f53 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 25 May 2026 10:47:22 +0000 Subject: [PATCH] Revert "Add a recovery path for autosaved documents that don't open successfully (#4157)" This reverts commit 62287d789324245ef552e4a4776e8e6106f8cf4a. --- Cargo.lock | 19 -- Cargo.toml | 1 - editor/Cargo.toml | 1 - .../failed_to_load_documents_dialog.rs | 113 -------- .../failed_to_open_document_dialog.rs | 84 ------ .../src/messages/dialog/simple_dialogs/mod.rs | 4 - .../portfolio/document/utility_types/error.rs | 8 +- .../messages/portfolio/portfolio_message.rs | 6 - .../portfolio/portfolio_message_handler.rs | 250 +----------------- 9 files changed, 15 insertions(+), 471 deletions(-) delete mode 100644 editor/src/messages/dialog/simple_dialogs/failed_to_load_documents_dialog.rs delete mode 100644 editor/src/messages/dialog/simple_dialogs/failed_to_open_document_dialog.rs diff --git a/Cargo.lock b/Cargo.lock index e465c40ff7..1e3e967f25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2192,7 +2192,6 @@ dependencies = [ "wasm-bindgen", "web-sys", "wgpu-executor", - "zip", ] [[package]] @@ -5958,12 +5957,6 @@ dependencies = [ "core_maths", ] -[[package]] -name = "typed-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" - [[package]] name = "typeid" version = "1.0.3" @@ -7662,18 +7655,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zip" -version = "8.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" -dependencies = [ - "crc32fast", - "indexmap", - "memchr", - "typed-path", -] - [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index a7e084372d..412ffcee74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -221,7 +221,6 @@ lzma-rust2 = { version = "0.16", default-features = false, features = ["std", "e scraper = "0.25" linesweeper = "0.3" smallvec = "1.13.2" -zip = { version = "8", default-features = false } [workspace.lints.rust] unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] } diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 1dfd7eac65..fb83dcd832 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -47,7 +47,6 @@ base64 = { workspace = true } spin = { workspace = true } image = { workspace = true } color = { workspace = true } -zip = { workspace = true } # Optional local dependencies wgpu-executor = { workspace = true, optional = true } diff --git a/editor/src/messages/dialog/simple_dialogs/failed_to_load_documents_dialog.rs b/editor/src/messages/dialog/simple_dialogs/failed_to_load_documents_dialog.rs deleted file mode 100644 index 4181c8f9fd..0000000000 --- a/editor/src/messages/dialog/simple_dialogs/failed_to_load_documents_dialog.rs +++ /dev/null @@ -1,113 +0,0 @@ -// TODO: Eventually remove this document upgrade code - -use crate::messages::layout::utility_types::widget_prelude::*; -use crate::messages::prelude::*; - -pub struct FailedToLoadDocumentsDialog { - pub failed_document_names: Vec, -} - -impl DialogLayoutHolder for FailedToLoadDocumentsDialog { - const ICON: &'static str = "Warning"; - const TITLE: &'static str = "Failed to Open Documents"; - - fn layout_buttons(&self) -> Layout { - let widgets = vec![ - TextButton::new("Download") - .emphasized(true) - .tooltip_description("Save the raw document data to disk so it can be recovered later.") - .on_update(|_| { - DialogMessage::CloseAndThen { - followups: vec![PortfolioMessage::DownloadFailedToLoadDocuments.into()], - } - .into() - }) - .widget_instance(), - TextButton::new("Discard") - .tooltip_description("Permanently delete the autosaved data for these documents.") - .on_update(|_| { - DialogMessage::CloseAndThen { - followups: vec![PortfolioMessage::DiscardFailedToLoadDocuments.into()], - } - .into() - }) - .widget_instance(), - TextButton::new("Dismiss") - .tooltip_description("Close this dialog. The autosaved data is kept and this dialog will reappear on next launch.") - .on_update(|_| FrontendMessage::DialogClose.into()) - .widget_instance(), - ]; - - Layout(vec![LayoutGroup::row(widgets)]) - } -} - -impl LayoutHolder for FailedToLoadDocumentsDialog { - fn layout(&self) -> Layout { - let count = self.failed_document_names.len(); - let header = format!("{count} document{} couldn't be reopened.", if count == 1 { "" } else { "s" }); - let list = "• ".to_string() + &self.failed_document_names.join("\n• "); - let plural_s = if count == 1 { "" } else { "s" }; - let plural_it_them = if count == 1 { "it" } else { "them" }; - - Layout(vec![ - LayoutGroup::row(vec![TextLabel::new(header).bold(true).multiline(true).widget_instance()]), - LayoutGroup::row(vec![ - TextLabel::new(format!( - "Sorry about that!\n\ - This shouldn't happen, and we'd like to help.\n\ - \n\ - Click \"Download\" to save a copy of the affected file{plural_s},\n\ - then please share {plural_it_them} with us so we can investigate:" - )) - .multiline(true) - .widget_instance(), - ]), - LayoutGroup::row(vec![ - TextButton::new("Ask on Discord") - .icon("Volunteer") - .flush(true) - .on_update(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://discord.graphite.art".into(), - } - .into() - }) - .widget_instance(), - ]), - LayoutGroup::row(vec![ - TextButton::new("Report on GitHub") - .icon("Bug") - .flush(true) - .on_update(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(), - } - .into() - }) - .widget_instance(), - ]), - LayoutGroup::row(vec![ - TextLabel::new( - "In the meantime, you can keep working in the\n\ - previous version of Graphite:", - ) - .multiline(true) - .widget_instance(), - ]), - LayoutGroup::row(vec![ - TextButton::new("Sept. 2025 Release") - .icon("GraphiteLogo") - .flush(true) - .on_update(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://57130155.graphite.pages.dev/".into(), - } - .into() - }) - .widget_instance(), - ]), - LayoutGroup::row(vec![TextLabel::new(format!("Affected document{plural_s}:\n{list}")).multiline(true).widget_instance()]), - ]) - } -} diff --git a/editor/src/messages/dialog/simple_dialogs/failed_to_open_document_dialog.rs b/editor/src/messages/dialog/simple_dialogs/failed_to_open_document_dialog.rs deleted file mode 100644 index c6caeadcfd..0000000000 --- a/editor/src/messages/dialog/simple_dialogs/failed_to_open_document_dialog.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::messages::layout::utility_types::widget_prelude::*; -use crate::messages::prelude::*; - -pub struct FailedToOpenDocumentDialog { - pub document_name: String, -} - -impl DialogLayoutHolder for FailedToOpenDocumentDialog { - const ICON: &'static str = "Warning"; - const TITLE: &'static str = "Failed to Open Document"; - - fn layout_buttons(&self) -> Layout { - let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()]; - Layout(vec![LayoutGroup::row(widgets)]) - } -} - -impl LayoutHolder for FailedToOpenDocumentDialog { - fn layout(&self) -> Layout { - let header = if self.document_name.trim().is_empty() { - "The document couldn't be opened.".to_string() - } else { - format!("\"{}\" couldn't be opened.", self.document_name) - }; - - Layout(vec![ - LayoutGroup::row(vec![TextLabel::new(header).bold(true).multiline(true).widget_instance()]), - LayoutGroup::row(vec![ - TextLabel::new( - "Sorry about that!\n\ - This shouldn't happen, and we'd like to help.\n\ - \n\ - Please share the file with us so we can investigate:", - ) - .multiline(true) - .widget_instance(), - ]), - LayoutGroup::row(vec![ - TextButton::new("Ask on Discord") - .icon("Volunteer") - .flush(true) - .on_update(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://discord.graphite.art".into(), - } - .into() - }) - .widget_instance(), - ]), - LayoutGroup::row(vec![ - TextButton::new("Report on GitHub") - .icon("Bug") - .flush(true) - .on_update(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(), - } - .into() - }) - .widget_instance(), - ]), - LayoutGroup::row(vec![ - TextLabel::new( - "In the meantime, you can keep working in the\n\ - previous version of Graphite:", - ) - .multiline(true) - .widget_instance(), - ]), - LayoutGroup::row(vec![ - TextButton::new("Sept. 2025 Release") - .icon("GraphiteLogo") - .flush(true) - .on_update(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://57130155.graphite.pages.dev/".into(), - } - .into() - }) - .widget_instance(), - ]), - ]) - } -} diff --git a/editor/src/messages/dialog/simple_dialogs/mod.rs b/editor/src/messages/dialog/simple_dialogs/mod.rs index aa2e8103c3..f775faec62 100644 --- a/editor/src/messages/dialog/simple_dialogs/mod.rs +++ b/editor/src/messages/dialog/simple_dialogs/mod.rs @@ -4,8 +4,6 @@ mod close_document_dialog; mod confirm_restart_dialog; mod demo_artwork_dialog; mod error_dialog; -mod failed_to_load_documents_dialog; -mod failed_to_open_document_dialog; mod licenses_dialog; mod licenses_third_party_dialog; @@ -16,7 +14,5 @@ pub use confirm_restart_dialog::ConfirmRestartDialog; pub use demo_artwork_dialog::ARTWORK; pub use demo_artwork_dialog::DemoArtworkDialog; pub use error_dialog::ErrorDialog; -pub use failed_to_load_documents_dialog::FailedToLoadDocumentsDialog; -pub use failed_to_open_document_dialog::FailedToOpenDocumentDialog; pub use licenses_dialog::LicensesDialog; pub use licenses_third_party_dialog::LicensesThirdPartyDialog; diff --git a/editor/src/messages/portfolio/document/utility_types/error.rs b/editor/src/messages/portfolio/document/utility_types/error.rs index f1b5ad3ffc..5080f46381 100644 --- a/editor/src/messages/portfolio/document/utility_types/error.rs +++ b/editor/src/messages/portfolio/document/utility_types/error.rs @@ -16,7 +16,13 @@ pub enum EditorError { #[error("The operation caused a document error:\n{0:?}")] Document(String), - #[error("Failed to deserialize document: {0}")] + #[error( + "This document was created in an older version of the editor.\n\ + \n\ + Full backwards compatibility is not guaranteed in the current alpha release.\n\ + \n\ + If this document is critical, ask for support in Graphite's Discord community." + )] DocumentDeserialization(String), #[error("{0}")] diff --git a/editor/src/messages/portfolio/portfolio_message.rs b/editor/src/messages/portfolio/portfolio_message.rs index 2b42a972b0..629c6332d5 100644 --- a/editor/src/messages/portfolio/portfolio_message.rs +++ b/editor/src/messages/portfolio/portfolio_message.rs @@ -72,12 +72,6 @@ pub enum PortfolioMessage { document_id: DocumentId, document_serialized_content: String, }, - // TODO: Eventually remove this document upgrade code - ShowFailedToLoadDocumentsDialog, - // TODO: Eventually remove this document upgrade code - DiscardFailedToLoadDocuments, - // TODO: Eventually remove this document upgrade code - DownloadFailedToLoadDocuments, MoveAllPanelTabs { source_group: PanelGroupId, target_group: PanelGroupId, diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 4cb781a7bb..219c05d06c 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -55,16 +55,6 @@ pub struct PortfolioMessageContext<'a> { pub struct PortfolioMessageHandler { pub documents: HashMap, unloaded_documents: HashMap, - /// Pairs of `(info, raw serialized content)` for autosaved documents that failed to deserialize. - /// The info entries are folded back into `persisted_state_snapshot` so their on-disk autosave files survive garbage collection. - // TODO: Eventually remove this document upgrade code - failed_to_load_documents: HashMap, - /// In-flight count of autosaved-document loads from the initial startup batch; the batched failure dialog fires when this hits 0. - // TODO: Eventually remove this document upgrade code - pending_initial_autosave_loads: usize, - /// Background eager loads whose trailing `SelectDocument` should be suppressed to keep focus on the user's active doc. - // TODO: Eventually remove this document upgrade code - pending_eager_loads: HashSet, document_ids: VecDeque, pub(crate) active_document_id: Option, persistent_state: PersistentStateMessageHandler, @@ -503,17 +493,11 @@ impl MessageHandler> for Portfolio workspace_layout: _, } = state; - // TODO: Eventually remove this document upgrade code - let mut newly_unloaded_ids = Vec::new(); - for info in documents { if !self.document_ids.contains(&info.id) { self.document_ids.push_back(info.id); } - if !self.documents.contains_key(&info.id) && !self.unloaded_documents.contains_key(&info.id) { - // TODO: Eventually remove this document upgrade code - newly_unloaded_ids.push(info.id); - + if !self.documents.contains_key(&info.id) { self.unloaded_documents.insert(info.id, info); } } @@ -521,27 +505,9 @@ impl MessageHandler> for Portfolio responses.add(PortfolioMessage::UpdateOpenDocumentsList); let select_document_id = current_document.filter(|id| self.document_ids.contains(id)).or_else(|| self.document_ids.front().copied()); - - // Eagerly load every autosaved doc on startup so deserialization failures can be reported in one batched dialog at the end. - // The active doc's read is deferred to the `SelectDocument` below, but is still counted. - // TODO: Eventually remove this document upgrade code - self.pending_initial_autosave_loads = self.pending_initial_autosave_loads.saturating_add(newly_unloaded_ids.len()); - - // TODO: Eventually remove this document upgrade code - for document_id in &newly_unloaded_ids { - if Some(*document_id) != select_document_id { - self.pending_eager_loads.insert(*document_id); - responses.add(PersistentStateMessage::ReadDocument { document_id: *document_id }); - } - } - if let Some(document_id) = select_document_id { responses.add(PortfolioMessage::SelectDocument { document_id }); } - // TODO: Eventually remove this document upgrade code - else if self.pending_initial_autosave_loads == 0 && !self.failed_to_load_documents.is_empty() { - responses.add(PortfolioMessage::ShowFailedToLoadDocumentsDialog); - } } PortfolioMessage::LoadDocumentContent { document_id, @@ -560,85 +526,7 @@ impl MessageHandler> for Portfolio document_is_saved: info.is_saved, document_serialized_content, }); - - // Suppress auto-select for startup eager loads to keep focus on the user's active doc - // TODO: Eventually remove this document upgrade code - // TODO: (But keep the inner logic unconditionally, just remove the condition) - if !self.pending_eager_loads.remove(&document_id) { - responses.add(PortfolioMessage::SelectDocument { document_id }); - } - } - // TODO: Eventually remove this document upgrade code - PortfolioMessage::ShowFailedToLoadDocumentsDialog => { - if self.failed_to_load_documents.is_empty() { - return; - } - let failed_document_names = self.failed_to_load_documents.values().map(|(info, _)| display_name_with_fallback(info)).collect(); - let dialog = simple_dialogs::FailedToLoadDocumentsDialog { failed_document_names }; - dialog.send_dialog_to_frontend(responses); - } - // TODO: Eventually remove this document upgrade code - PortfolioMessage::DiscardFailedToLoadDocuments => { - let failed = std::mem::take(&mut self.failed_to_load_documents); - for document_id in failed.keys() { - self.document_ids.retain(|id| id != document_id); - responses.add(PersistentStateMessage::DeleteDocument { document_id: *document_id }); - } - responses.add(PortfolioMessage::UpdateOpenDocumentsList); - responses.add(PersistentStateMessage::WriteState); - } - // TODO: Eventually remove this document upgrade code - PortfolioMessage::DownloadFailedToLoadDocuments => { - if self.failed_to_load_documents.is_empty() { - return; - } - - let mut used_names: HashMap = HashMap::new(); - let files: Vec<(String, Vec)> = self - .failed_to_load_documents - .values() - .map(|(info, content)| { - let stem = sanitize_filename_stem(&info.name).unwrap_or_else(|| format!("document-{:x}", info.id.0)); - let base = format!("{stem}.{FILE_EXTENSION}"); - let unique = match used_names.get(&base).copied() { - None => { - used_names.insert(base.clone(), 1); - base - } - Some(n) => { - used_names.insert(base.clone(), n + 1); - format!("{stem} ({n}).{FILE_EXTENSION}") - } - }; - (unique, content.as_bytes().to_vec()) - }) - .collect(); - - const FOLDER_NAME: &str = "Graphite Recovered Documents"; - - if files.len() == 1 { - let (filename, content) = files.into_iter().next().expect("just checked there's one entry"); - responses.add(FrontendMessage::TriggerSaveFile { - name: filename, - folder: None, - content: serde_bytes::ByteBuf::from(content), - }); - } else { - match build_recovery_zip(&files) { - Ok(zip_bytes) => responses.add(FrontendMessage::TriggerSaveFile { - name: format!("{FOLDER_NAME}.zip"), - folder: None, - content: serde_bytes::ByteBuf::from(zip_bytes), - }), - Err(e) => { - log::error!("Failed to build recovery zip: {e}"); - responses.add(DialogMessage::DisplayDialogError { - title: "Failed to download".to_string(), - description: format!("Could not bundle the failed documents for download.\n\n{e}"), - }); - } - } - } + responses.add(PortfolioMessage::SelectDocument { document_id }); } PortfolioMessage::NewDocumentWithName { name } => { let mut new_document = DocumentMessageHandler::default(); @@ -881,37 +769,11 @@ impl MessageHandler> for Portfolio let mut document = match document { Ok(document) => document, Err(e) => { - // TODO: Eventually remove this document upgrade code - // TODO: (Only the `if` branch, the `else` branch's manual-open dialog stays) - if document_is_auto_saved { - let name = document_name.unwrap_or_default(); - let info = DocumentInfo { - id: document_id, - name, - resources: None, - path: document_path, - is_saved: document_is_saved, - }; - self.document_ids.retain(|id| id != &document_id); - self.failed_to_load_documents.insert(document_id, (info, document_serialized_content)); - - if self.active_document_id == Some(document_id) { - self.active_document_id = None; - if let Some(next_id) = self.document_ids.front().copied() { - responses.add(PortfolioMessage::SelectDocument { document_id: next_id }); - } - } - - responses.add(PortfolioMessage::UpdateOpenDocumentsList); - self.tick_autosave_load_progress(responses, true); - } else { - log::error!("{e}"); - let name = document_name - .filter(|n| !n.trim().is_empty()) - .or_else(|| document_path.as_ref().and_then(|p| p.file_stem()).map(|s| s.to_string_lossy().into_owned())) - .unwrap_or_default(); - let dialog = simple_dialogs::FailedToOpenDocumentDialog { document_name: name }; - dialog.send_dialog_to_frontend(responses); + if !document_is_auto_saved { + responses.add(DialogMessage::DisplayDialogError { + title: "Failed to open document".to_string(), + description: e.to_string(), + }); } return; @@ -994,11 +856,6 @@ impl MessageHandler> for Portfolio self.load_document(document, document_id, responses); responses.add(AppWindowMessage::Focus); - - // TODO: Eventually remove this document upgrade code - if document_is_auto_saved { - self.tick_autosave_load_progress(responses, false); - } } PortfolioMessage::OpenImage { name, image } => { // `NewDocumentWithName`'s handler routes empty/None-equivalent names through `resolve_document_name` which assigns the next available "Untitled Document {N}". @@ -1908,13 +1765,7 @@ impl PortfolioMessageHandler { } pub fn persisted_state_snapshot(&self) -> PersistedState { - let mut documents = self.document_ids.iter().filter_map(|id| self.document_details(*id)).collect::>(); - - // Keep failed-to-load docs referenced in `state.documents` so their autosave files survive `garbage_collect_document_files` - // TODO: Eventually remove this document upgrade code - for (info, _) in self.failed_to_load_documents.values() { - documents.push(info.clone()); - } + let documents = self.document_ids.iter().filter_map(|id| self.document_details(*id)).collect::>(); PersistedState { documents, @@ -1958,18 +1809,6 @@ impl PortfolioMessageHandler { } } - // TODO: Eventually remove this document upgrade code - fn tick_autosave_load_progress(&mut self, responses: &mut VecDeque, failed: bool) { - if self.pending_initial_autosave_loads > 0 { - self.pending_initial_autosave_loads -= 1; - if self.pending_initial_autosave_loads == 0 && !self.failed_to_load_documents.is_empty() { - responses.add(PortfolioMessage::ShowFailedToLoadDocumentsDialog); - } - } else if failed { - responses.add(PortfolioMessage::ShowFailedToLoadDocumentsDialog); - } - } - fn read_file(path: &PathBuf, content: Vec) -> FileContent { let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or_default().to_lowercase(); match extension.as_str() { @@ -2179,76 +2018,3 @@ impl PortfolioMessageHandler { } } } - -// TODO: Eventually remove this document upgrade code -fn display_name_with_fallback(info: &DocumentInfo) -> String { - if info.name.trim().is_empty() { - format!("Untitled Document ({:x})", info.id.0) - } else { - info.name.clone() - } -} - -/// Returns `None` if the name has no safe filename characters left or matches a Windows reserved device name, so callers fall back to an ID-based stem. -// TODO: Eventually remove this document upgrade code -fn sanitize_filename_stem(name: &str) -> Option { - let replaced: String = name - .chars() - .map(|c| { - if matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') || c.is_control() { - '_' - } else { - c - } - }) - .collect(); - - // Trim dots to avoid `.` / `..` resolving against the parent directory, and to dodge Windows' trailing-dot/space quirks - let trimmed = replaced.trim().trim_matches('.').trim(); - if trimmed.is_empty() { - return None; - } - - // Windows rejects these regardless of extension; superscript digits are normalized equivalently by some path APIs - let first_segment = trimmed.split('.').next().unwrap_or("").to_ascii_uppercase(); - if matches!( - first_segment.as_str(), - "CON" - | "PRN" | "AUX" - | "NUL" | "COM1" - | "COM2" | "COM3" - | "COM4" | "COM5" - | "COM6" | "COM7" - | "COM8" | "COM9" - | "COM¹" | "COM²" - | "COM³" | "LPT1" - | "LPT2" | "LPT3" - | "LPT4" | "LPT5" - | "LPT6" | "LPT7" - | "LPT8" | "LPT9" - | "LPT¹" | "LPT²" - | "LPT³" - ) { - return None; - } - - Some(trimmed.to_string()) -} - -// TODO: Eventually remove this document upgrade code -fn build_recovery_zip(entries: &[(String, Vec)]) -> Result, String> { - use std::io::{Cursor, Write}; - use zip::write::{SimpleFileOptions, ZipWriter}; - - let mut buffer = Cursor::new(Vec::::new()); - let mut writer = ZipWriter::new(&mut buffer); - let options: SimpleFileOptions = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored).unix_permissions(0o644); - - for (filename, content) in entries { - writer.start_file(filename, options).map_err(|e| format!("start_file: {e}"))?; - writer.write_all(content).map_err(|e| format!("write_all: {e}"))?; - } - - writer.finish().map_err(|e| format!("finish: {e}"))?; - Ok(buffer.into_inner()) -}