chore: resolve SonarCloud maintainability issues (#1241)

* chore: resolve SonarCloud maintainability issues

- Define SHA256_FMT constant in packaging script to remove duplicated string literal (shelldre:S1192)
- Remove redundant 'static lifetime annotations from const items (rust:S8863)
- Replace closures with method and function pointers (rust:S1612)
- Extract helper functions in synthetic document generation, bezier polyline flattening, and component restoration to reduce cognitive complexity (rust:S3776)

* fix(packaging): use verify_sha256 helper function to avoid shellcheck SC2059

* fix(engine): remove duplicate closing brace in large_document

* fix(engine): take &mut Document in restore_id_map; keep 'static on associated consts

restore_id_map allocates ids, so it needs a mutable document. Elided
lifetimes in associated consts trip elided_lifetimes_in_associated_constant,
so those annotations are restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Assign positional parameters to locals in sha256 verification

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
This commit is contained in:
Dylan M. Taylor
2026-10-23 23:28:13 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Dylan Taylor
parent c668e21759
commit a1a842af9a
19 changed files with 139 additions and 102 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
//! The file subcommands: info, export, convert, render and run. //! The file subcommands: info, export, convert, render and run.
use std::ffi::OsStr;
use std::path::Path; use std::path::Path;
use std::process::ExitCode; use std::process::ExitCode;
@@ -19,7 +20,7 @@ fn load_noted(path: &str) -> CliResult<(Document, Vec<String>)> {
} }
fn ext(path: &str) -> String { fn ext(path: &str) -> String {
Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase() Path::new(path).extension().and_then(OsStr::to_str).unwrap_or("").to_ascii_lowercase()
} }
fn image_format(e: &str) -> Option<ExportFormat> { fn image_format(e: &str) -> Option<ExportFormat> {
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::store::Store;
use crate::test_util::{TempDir, config}; use crate::test_util::{TempDir, config};
fn args(s: &[&str]) -> Vec<String> { fn args(s: &[&str]) -> Vec<String> {
s.iter().map(|a| a.to_string()).collect() s.iter().map(ToString::to_string).collect()
} }
fn run(dir: &std::path::Path, command: Command) -> Result<String, String> { fn run(dir: &std::path::Path, command: Command) -> Result<String, String> {
+2 -1
View File
@@ -13,6 +13,7 @@
//! store replays such a log anyway and reports any change the snapshot lacked. A log ends at its //! store replays such a log anyway and reports any change the snapshot lacked. A log ends at its
//! first damaged record (a write cut short). //! first damaged record (a write cut short).
use std::ffi::OsStr;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -49,7 +50,7 @@ pub fn free_bytes(path: &Path) -> Option<u64> {
/// `path` with `.tmp` added: where its next version is written first. /// `path` with `.tmp` added: where its next version is written first.
pub fn temp_of(path: &Path) -> PathBuf { pub fn temp_of(path: &Path) -> PathBuf {
let mut name = path.file_name().map(|n| n.to_os_string()).unwrap_or_default(); let mut name = path.file_name().map(OsStr::to_os_string).unwrap_or_default();
name.push(".tmp"); name.push(".tmp");
path.with_file_name(name) path.with_file_name(name)
} }
+1 -1
View File
@@ -3,7 +3,7 @@
use crate::args::{Args, file_url_path, is_invite, parse}; use crate::args::{Args, file_url_path, is_invite, parse};
fn args(a: &[&str]) -> Vec<String> { fn args(a: &[&str]) -> Vec<String> {
a.iter().map(|s| s.to_string()).collect() a.iter().map(ToString::to_string).collect()
} }
#[test] #[test]
+1 -1
View File
@@ -103,7 +103,7 @@ impl BlendMode {
/// The mode named by its label or CSS keyword, ignoring case, spaces and dashes. Labels come /// The mode named by its label or CSS keyword, ignoring case, spaces and dashes. Labels come
/// first: "normal" is Normal, not pass-through (whose CSS keyword is also `normal`). /// first: "normal" is Normal, not pass-through (whose CSS keyword is also `normal`).
pub fn from_name(s: &str) -> Option<Self> { pub fn from_name(s: &str) -> Option<Self> {
let key: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>().to_ascii_lowercase(); let key: String = s.chars().filter(char::is_ascii_alphanumeric).collect::<String>().to_ascii_lowercase();
Self::ALL Self::ALL
.into_iter() .into_iter()
.find(|m| m.label().replace(' ', "").to_ascii_lowercase() == key) .find(|m| m.label().replace(' ', "").to_ascii_lowercase() == key)
+1 -1
View File
@@ -475,7 +475,7 @@ impl WidthPreset {
} }
pub fn from_id(s: &str) -> Option<WidthPreset> { pub fn from_id(s: &str) -> Option<WidthPreset> {
let s: String = s.chars().filter(|c| c.is_ascii_alphanumeric()).collect(); let s: String = s.chars().filter(char::is_ascii_alphanumeric).collect();
WidthPreset::ALL WidthPreset::ALL
.into_iter() .into_iter()
.find(|p| p.id().eq_ignore_ascii_case(&s) || p.label().replace(' ', "").eq_ignore_ascii_case(&s)) .find(|p| p.id().eq_ignore_ascii_case(&s) || p.label().replace(' ', "").eq_ignore_ascii_case(&s))
+34 -24
View File
@@ -786,6 +786,38 @@ fn push_changes(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"layers": pushed})) Ok(json!({"layers": pushed}))
} }
fn restore_prop_defs(doc: &Document, props: &BTreeMap<String, Value>) -> BTreeMap<String, PropDef> {
let mut defs = BTreeMap::new();
for (k, v) in props {
let def = match v {
Value::Bool(b) => PropDef::Boolean { default: *b },
Value::String(t) => PropDef::Text { default: t.clone() },
other => match id_of(other) {
Some(c) if doc.contains(c) => PropDef::InstanceSwap { default: c, preferred: Vec::new() },
_ => continue,
},
};
defs.insert(k.clone(), def);
}
defs
}
fn restore_id_map(doc: &mut Document, i: NodeId, component: NodeId, sub: &[NodeId]) -> BTreeMap<NodeId, NodeId> {
let mut map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
let mut used: BTreeSet<NodeId> = BTreeSet::new();
map.insert(i, component);
used.insert(component);
for d in sub.iter().filter(|d| **d != i) {
let src = doc.node(*d).and_then(|n| n.source);
let nid = match src {
Some(s) if !doc.contains(s) && used.insert(s) => s,
_ => doc.alloc_id(),
};
map.insert(*d, nid);
}
map
}
fn restore_main(s: &mut Session, p: &Value) -> Result<Value> { fn restore_main(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "component.restoreMain"; let cmd = "component.restoreMain";
let i = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select an instance"))?; let i = targets(s, p).first().copied().ok_or_else(|| bad(cmd, "select an instance"))?;
@@ -804,35 +836,13 @@ fn restore_main(s: &mut Session, p: &Value) -> Result<Value> {
// The main comes back with its old id (other instances point to it) and its layers with the // The main comes back with its old id (other instances point to it) and its layers with the
// ids their copies mirror, so every instance re-links by source. // ids their copies mirror, so every instance re-links by source.
let sub = doc.subtree(i); let sub = doc.subtree(i);
let mut map: BTreeMap<NodeId, NodeId> = BTreeMap::new(); let map = restore_id_map(doc, i, component, &sub);
let mut used: BTreeSet<NodeId> = BTreeSet::new();
map.insert(i, component);
used.insert(component);
for d in sub.iter().filter(|d| **d != i) {
let src = doc.node(*d).and_then(|n| n.source);
let nid = match src {
Some(s) if !doc.contains(s) && used.insert(s) => s,
_ => doc.alloc_id(),
};
map.insert(*d, nid);
}
let mut nodes = Vec::new(); let mut nodes = Vec::new();
let mut nested = Vec::new(); let mut nested = Vec::new();
for d in &sub { for d in &sub {
let Some(mut n) = doc.node(*d).cloned() else { continue }; let Some(mut n) = doc.node(*d).cloned() else { continue };
if *d == i { if *d == i {
let mut defs = BTreeMap::new(); let defs = restore_prop_defs(doc, &props);
for (k, v) in &props {
let def = match v {
Value::Bool(b) => PropDef::Boolean { default: *b },
Value::String(t) => PropDef::Text { default: t.clone() },
other => match id_of(other) {
Some(c) if doc.contains(c) => PropDef::InstanceSwap { default: c, preferred: Vec::new() },
_ => continue,
},
};
defs.insert(k.clone(), def);
}
n.kind = NodeKind::Component { description: String::new(), props: defs }; n.kind = NodeKind::Component { description: String::new(), props: defs };
n.source = None; n.source = None;
n.overrides.clear(); n.overrides.clear();
+2 -2
View File
@@ -488,11 +488,11 @@ pub(crate) fn write(path: &str, _bytes: &[u8]) -> Result<()> {
} }
fn ext(path: &str) -> String { fn ext(path: &str) -> String {
std::path::Path::new(path).extension().and_then(|e| e.to_str()).unwrap_or("").to_ascii_lowercase() std::path::Path::new(path).extension().and_then(std::ffi::OsStr::to_str).unwrap_or("").to_ascii_lowercase()
} }
fn file_stem(path: &str) -> String { fn file_stem(path: &str) -> String {
std::path::Path::new(path).file_stem().and_then(|e| e.to_str()).unwrap_or("Untitled").to_string() std::path::Path::new(path).file_stem().and_then(std::ffi::OsStr::to_str).unwrap_or("Untitled").to_string()
} }
/// The counts the import report shows for a freshly loaded document. /// The counts the import report shows for a freshly loaded document.
+1 -1
View File
@@ -9,7 +9,7 @@ pub fn run<T>(cmd: &str, f: impl FnOnce() -> Result<T>) -> Result<T> {
Err(payload) => { Err(payload) => {
let msg = payload let msg = payload
.downcast_ref::<&str>() .downcast_ref::<&str>()
.map(|s| s.to_string()) .map(ToString::to_string)
.or_else(|| payload.downcast_ref::<String>().cloned()) .or_else(|| payload.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "panic".to_string()); .unwrap_or_else(|| "panic".to_string());
log::error!("command `{cmd}` panicked: {msg}"); log::error!("command `{cmd}` panicked: {msg}");
+1 -1
View File
@@ -339,7 +339,7 @@ pub fn publish(doc: &Document, prev: Option<&PublishedLibrary>, sel: &Selection,
.map(|v| v.id.as_str()) .map(|v| v.id.as_str())
.collect(); .collect();
let (bound, mode_colls) = used_variables(doc, &units); let (bound, mode_colls) = used_variables(doc, &units);
let var_ids = alias_closure(&doc.variables, offered_vars.iter().map(|v| v.to_string()).chain(bound)); let var_ids = alias_closure(&doc.variables, offered_vars.iter().map(ToString::to_string).chain(bound));
snap.variables.variables = doc.variables.variables.iter().filter(|v| var_ids.contains(&v.id)).cloned().collect(); snap.variables.variables = doc.variables.variables.iter().filter(|v| var_ids.contains(&v.id)).cloned().collect();
let coll_ids: BTreeSet<&str> = snap let coll_ids: BTreeSet<&str> = snap
.variables .variables
+50 -35
View File
@@ -28,6 +28,53 @@ fn fill(doc: &mut Document, id: NodeId, c: Color) {
} }
} }
fn add_screen_header(doc: &mut Document, screen: NodeId, s: usize) -> DocResult<()> {
let header = doc.create(screen, NodeKind::Frame, "Header", 0.0, 0.0, SCREEN_W, 40.0)?;
if let Some(n) = doc.node_mut(header) {
n.layout = Some(AutoLayout { gap: 8.0, padding: [8.0; 4], ..AutoLayout::default() });
n.fills = vec![Paint::solid(Color::from_rgb8_const(0xF5, 0xF5, 0xF5))];
}
doc.create(header, text(&format!("Screen {}", s + 1)), "Title", 0.0, 0.0, 120.0, 16.0)?;
for k in 0..2 {
let icon = doc.create(header, ellipse(), &format!("Icon {}", k + 1), 0.0, 0.0, 20.0, 20.0)?;
fill(doc, icon, Color::from_rgb8_const(0x99, 0x99, 0x99));
}
Ok(())
}
fn add_card_group(doc: &mut Document, screen: NodeId) -> DocResult<()> {
let card = doc.create(screen, NodeKind::Group, "Card", 12.0, 52.0, 160.0, 64.0)?;
// Children are in the group's space; normalising fits the group around them.
let cbg = doc.create(card, NodeKind::Rectangle, "Card background", 0.0, 0.0, 160.0, 64.0)?;
fill(doc, cbg, Color::from_rgb8_const(0xEE, 0xF4, 0xFF));
doc.create(card, ellipse(), "Avatar", 8.0, 8.0, 32.0, 32.0)?;
doc.create(card, text("Ada Lovelace"), "Name", 48.0, 8.0, 100.0, 16.0)?;
doc.create(card, text("Analyst"), "Role", 48.0, 28.0, 100.0, 16.0)?;
Ok(())
}
fn add_screen_grid(doc: &mut Document, screen: NodeId, s: usize, rest: usize, made: &mut usize) -> DocResult<()> {
for k in 0..rest {
let (gx, gy) = (184.0 + (k % 8) as f64 * 21.0, 52.0 + (k / 8) as f64 * 34.0);
let hue = ((s * PER_SCREEN + k) % 360) as f32 / 360.0;
// The grid starts with a rectangle so even the smallest workload has one.
let kind = (k + 2) % 5;
let id = match kind {
0 => doc.create(screen, text(&format!("Item {k}")), "Text", gx, gy, 20.0, 14.0)?,
1 | 3 => doc.create(screen, ellipse(), "Ellipse", gx, gy, 18.0, 18.0)?,
_ => doc.create(screen, NodeKind::Rectangle, "Rectangle", gx, gy, 18.0, 18.0)?,
};
if kind != 0 {
fill(doc, id, Color::new(hue, 0.5, 1.0 - hue, 1.0));
if let Some(n) = doc.node_mut(id) {
n.corners.radii = [3.0; 4];
}
}
*made += 1;
}
Ok(())
}
/// A page of about `layers` layers (pages not counted), normalised like an opened file. Returns /// A page of about `layers` layers (pages not counted), normalised like an opened file. Returns
/// the document and its page. /// the document and its page.
pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> { pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
@@ -53,28 +100,13 @@ pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
let budget = |made: usize| layers.saturating_sub(made); let budget = |made: usize| layers.saturating_sub(made);
// Auto layout header: title + two icons (4 layers). // Auto layout header: title + two icons (4 layers).
if budget(made) >= 4 { if budget(made) >= 4 {
let header = doc.create(screen, NodeKind::Frame, "Header", 0.0, 0.0, SCREEN_W, 40.0)?; add_screen_header(&mut doc, screen, s)?;
if let Some(n) = doc.node_mut(header) {
n.layout = Some(AutoLayout { gap: 8.0, padding: [8.0; 4], ..AutoLayout::default() });
n.fills = vec![Paint::solid(Color::from_rgb8_const(0xF5, 0xF5, 0xF5))];
}
doc.create(header, text(&format!("Screen {}", s + 1)), "Title", 0.0, 0.0, 120.0, 16.0)?;
for k in 0..2 {
let icon = doc.create(header, ellipse(), &format!("Icon {}", k + 1), 0.0, 0.0, 20.0, 20.0)?;
fill(&mut doc, icon, Color::from_rgb8_const(0x99, 0x99, 0x99));
}
made += 4; made += 4;
} }
// A card group (5 layers). // A card group (5 layers).
// The card and the instances leave one layer for the grid, which starts with a rectangle. // The card and the instances leave one layer for the grid, which starts with a rectangle.
if budget(made) > 5 { if budget(made) > 5 {
let card = doc.create(screen, NodeKind::Group, "Card", 12.0, 52.0, 160.0, 64.0)?; add_card_group(&mut doc, screen)?;
// Children are in the group's space; normalising fits the group around them.
let cbg = doc.create(card, NodeKind::Rectangle, "Card background", 0.0, 0.0, 160.0, 64.0)?;
fill(&mut doc, cbg, Color::from_rgb8_const(0xEE, 0xF4, 0xFF));
doc.create(card, ellipse(), "Avatar", 8.0, 8.0, 32.0, 32.0)?;
doc.create(card, text("Ada Lovelace"), "Name", 48.0, 8.0, 100.0, 16.0)?;
doc.create(card, text("Analyst"), "Role", 48.0, 28.0, 100.0, 16.0)?;
made += 5; made += 5;
} }
// Three button instances (3 layers each). // Three button instances (3 layers each).
@@ -87,24 +119,7 @@ pub fn large_document(layers: usize) -> DocResult<(Document, NodeId)> {
} }
// Fill the rest of the screen with a grid of shapes and text. // Fill the rest of the screen with a grid of shapes and text.
let rest = PER_SCREEN.saturating_sub(4 + 5 + 9 + 1).min(budget(made)); let rest = PER_SCREEN.saturating_sub(4 + 5 + 9 + 1).min(budget(made));
for k in 0..rest { add_screen_grid(&mut doc, screen, s, rest, &mut made)?;
let (gx, gy) = (184.0 + (k % 8) as f64 * 21.0, 52.0 + (k / 8) as f64 * 34.0);
let hue = ((s * PER_SCREEN + k) % 360) as f32 / 360.0;
// The grid starts with a rectangle so even the smallest workload has one.
let kind = (k + 2) % 5;
let id = match kind {
0 => doc.create(screen, text(&format!("Item {k}")), "Text", gx, gy, 20.0, 14.0)?,
1 | 3 => doc.create(screen, ellipse(), "Ellipse", gx, gy, 18.0, 18.0)?,
_ => doc.create(screen, NodeKind::Rectangle, "Rectangle", gx, gy, 18.0, 18.0)?,
};
if kind != 0 {
fill(&mut doc, id, Color::new(hue, 0.5, 1.0 - hue, 1.0));
if let Some(n) = doc.node_mut(id) {
n.corners.radii = [3.0; 4];
}
}
made += 1;
}
if budget(made) == 0 { if budget(made) == 0 {
break; break;
} }
+1 -1
View File
@@ -216,7 +216,7 @@ impl Kit {
pub fn variables(&mut self, collection: &str, modes: &[&str], vars: Vec<(&str, &str, Vec<Value>)>) -> Result<()> { pub fn variables(&mut self, collection: &str, modes: &[&str], vars: Vec<(&str, &str, Vec<Value>)>) -> Result<()> {
self.run("variable.createCollection", json!({"name": collection, "modes": modes}))?; self.run("variable.createCollection", json!({"name": collection, "modes": modes}))?;
for (name, kind, values) in vars { for (name, kind, values) in vars {
let values: Map<String, Value> = modes.iter().map(|m| m.to_string()).zip(values).collect(); let values: Map<String, Value> = modes.iter().map(ToString::to_string).zip(values).collect();
self.run("variable.create", json!({"collection": collection, "name": name, "type": kind, "values": values}))?; self.run("variable.create", json!({"collection": collection, "name": name, "type": kind, "values": values}))?;
} }
Ok(()) Ok(())
+22 -19
View File
@@ -91,6 +91,27 @@ pub fn polylines(path: &BezPath) -> Option<Vec<Polyline>> {
polylines_with_limit(path, MAX_SAMPLES) polylines_with_limit(path, MAX_SAMPLES)
} }
fn flatten_curve_segment(el: PathEl, seg_start: Point, limit: usize, count: &mut usize, cur: &mut Polyline) -> bool {
let mut over = false;
kurbo::flatten([PathEl::MoveTo(seg_start), el], FLATTEN_TOL, |fel| {
if over {
return;
}
if let PathEl::LineTo(p) = fel {
*count += 1;
if *count > limit {
over = true;
return;
}
if cur.points.is_empty() {
cur.push(seg_start);
}
cur.push(p);
}
});
!over
}
/// [`polylines`] with an explicit sample budget, so tests can use a small one. /// [`polylines`] with an explicit sample budget, so tests can use a small one.
/// ///
/// The budget is enforced *while* flattening, not after it (issue #204): the input element /// The budget is enforced *while* flattening, not after it (issue #204): the input element
@@ -141,25 +162,7 @@ fn polylines_with_limit(path: &BezPath, limit: usize) -> Option<Vec<Polyline>> {
PathEl::QuadTo(_, end) | PathEl::CurveTo(_, _, end) => { PathEl::QuadTo(_, end) | PathEl::CurveTo(_, _, end) => {
// Flatten this one segment on its own, so an over-budget curve aborts // Flatten this one segment on its own, so an over-budget curve aborts
// here instead of after the whole path has been flattened. // here instead of after the whole path has been flattened.
let seg_start = pen; if !flatten_curve_segment(*el, pen, limit, &mut count, &mut cur) {
let mut over = false;
kurbo::flatten([PathEl::MoveTo(seg_start), *el], FLATTEN_TOL, |fel| {
if over {
return;
}
if let PathEl::LineTo(p) = fel {
count += 1;
if count > limit {
over = true;
return;
}
if cur.points.is_empty() {
cur.push(seg_start);
}
cur.push(p);
}
});
if over {
return None; return None;
} }
pen = end; pen = end;
+1 -1
View File
@@ -59,7 +59,7 @@ fn error(id: Value, code: i64, message: impl Into<String>) -> Value {
} }
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String { fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
payload.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into()) payload.downcast_ref::<&str>().map(ToString::to_string).or_else(|| payload.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into())
} }
impl Server { impl Server {
+1 -1
View File
@@ -812,7 +812,7 @@ fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap<String, (Str
if !meta.is_file() { if !meta.is_file() {
continue; continue;
} }
let ext = path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).unwrap_or_default(); let ext = path.extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default();
if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") { if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") {
continue; continue;
} }
+6 -5
View File
@@ -233,6 +233,7 @@ mod windows_qa_tests;
mod windows_tests; mod windows_tests;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::future::Future; use std::future::Future;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::pin::Pin; use std::pin::Pin;
@@ -1175,7 +1176,7 @@ impl FigApp {
pub fn save_as_dialog(&mut self) { pub fn save_as_dialog(&mut self) {
let Ok(name) = self.session.doc().map(|d| d.name.clone()) else { return }; let Ok(name) = self.session.doc().map(|d| d.name.clone()) else { return };
self.pick_save(i18n::tr("Save as"), &format!("{name}.figcraft"), &["figcraft", "fig"], |app, mut p| { self.pick_save(i18n::tr("Save as"), &format!("{name}.figcraft"), &["figcraft", "fig"], |app, mut p| {
if p.extension().is_none_or(|e| e.is_empty()) { if p.extension().is_none_or(OsStr::is_empty) {
if !p.set_extension("figcraft") { if !p.set_extension("figcraft") {
app.toast(i18n::tr("Use the .figcraft or .fig extension.")); app.toast(i18n::tr("Use the .figcraft or .fig extension."));
return; return;
@@ -1192,7 +1193,7 @@ impl FigApp {
return; return;
} }
} }
} else if !p.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("figcraft") || e.eq_ignore_ascii_case("fig")) { } else if !p.extension().and_then(OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("figcraft") || e.eq_ignore_ascii_case("fig")) {
app.toast(i18n::tr("Use the .figcraft or .fig extension.")); app.toast(i18n::tr("Use the .figcraft or .fig extension."));
return; return;
} }
@@ -1211,7 +1212,7 @@ impl FigApp {
.docs .docs
.get(tab) .get(tab)
.and_then(|d| d.path.as_deref()) .and_then(|d| d.path.as_deref())
.is_some_and(|path| !Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("fig"))); .is_some_and(|path| !Path::new(path).extension().and_then(OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("fig")));
if native_path { if native_path {
if self.run("file.save", json!({})).is_some() { if self.run("file.save", json!({})).is_some() {
self.toast(i18n::tr("Saved")); self.toast(i18n::tr("Saved"));
@@ -1870,13 +1871,13 @@ pub(crate) const OPEN_EXTENSIONS: &[&str] = &["figcraft", "fig", "json", "svg",
/// Whether `path` names a file type that opens as a document (the extension, in any case). /// Whether `path` names a file type that opens as a document (the extension, in any case).
pub(crate) fn opens(path: &Path) -> bool { pub(crate) fn opens(path: &Path) -> bool {
path.extension().and_then(|e| e.to_str()).is_some_and(|e| OPEN_EXTENSIONS.iter().any(|x| x.eq_ignore_ascii_case(e))) path.extension().and_then(OsStr::to_str).is_some_and(|e| OPEN_EXTENSIONS.iter().any(|x| x.eq_ignore_ascii_case(e)))
} }
/// Whether `path` is a whole document (`.figcraft`, `.fig`, file JSON) rather than an image or SVG: /// Whether `path` is a whole document (`.figcraft`, `.fig`, file JSON) rather than an image or SVG:
/// dropped on the canvas it opens as a file of its own instead of adding its pages to the open one. /// dropped on the canvas it opens as a file of its own instead of adding its pages to the open one.
pub(crate) fn is_document(path: &Path) -> bool { pub(crate) fn is_document(path: &Path) -> bool {
path.extension().and_then(|e| e.to_str()).is_some_and(|e| ["figcraft", "fig", "json"].iter().any(|x| x.eq_ignore_ascii_case(e))) path.extension().and_then(OsStr::to_str).is_some_and(|e| ["figcraft", "fig", "json"].iter().any(|x| x.eq_ignore_ascii_case(e)))
} }
/// Reads the contents of dropped file `name` that has no file on disk to read by path. /// Reads the contents of dropped file `name` that has no file on disk to read by path.
+1 -1
View File
@@ -95,7 +95,7 @@ pub fn guard(app: &mut FigApp, ctx: &Context, f: impl FnOnce(&mut FigApp)) {
let roots = app.automation_roots.clone(); let roots = app.automation_roots.clone();
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| figcraft_engine::file_access::confined(roots.as_ref(), || f(&mut *app)))); let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| figcraft_engine::file_access::confined(roots.as_ref(), || f(&mut *app))));
if let Err(e) = res { if let Err(e) = res {
let msg = e.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| e.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into()); let msg = e.downcast_ref::<&str>().map(ToString::to_string).or_else(|| e.downcast_ref::<String>().cloned()).unwrap_or_else(|| "panic".into());
log::error!("UI panic: {msg}"); log::error!("UI panic: {msg}");
app.toast(format!("Internal error (please report): {msg}")); app.toast(format!("Internal error (please report): {msg}"));
ctx.request_repaint(); ctx.request_repaint();
+10 -4
View File
@@ -25,6 +25,12 @@ set -euo pipefail
HERE="$ROOT/packaging/linux" HERE="$ROOT/packaging/linux"
APP_ID=ai.storyteller.figcraft APP_ID=ai.storyteller.figcraft
verify_sha256() {
local expected="$1"
local file="$2"
printf '%s %s\n' "$expected" "$file" | sha256sum -c -
}
SKIP_BUILD=0 SKIP_BUILD=0
FORMATS="appimage deb rpm tar" FORMATS="appimage deb rpm tar"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -145,21 +151,21 @@ if has appimage; then
download="$TOOL.download" download="$TOOL.download"
curl --proto '=https' -fsSL -o "$download" \ curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage"
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c - verify_sha256 "$APPIMAGETOOL_SHA256" "$download"
chmod 755 "$download" chmod 755 "$download"
mv "$download" "$TOOL" mv "$download" "$TOOL"
fi fi
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c - verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL"
fi fi
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH" RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
if [[ ! -f "$RUNTIME" ]]; then if [[ ! -f "$RUNTIME" ]]; then
download="$RUNTIME.download" download="$RUNTIME.download"
curl --proto '=https' -fsSL -o "$download" \ curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH" "https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH"
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c - verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$download"
mv "$download" "$RUNTIME" mv "$download" "$RUNTIME"
fi fi
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c - verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME"
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
# relative, e.g. target/agent-<name>). # relative, e.g. target/agent-<name>).
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage" OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
+1 -1
View File
@@ -31,7 +31,7 @@ fn cargo() -> Command {
} }
fn root() -> std::path::PathBuf { fn root() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(|p| p.to_path_buf()).unwrap_or_default() std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(std::path::Path::to_path_buf).unwrap_or_default()
} }
fn main() -> ExitCode { fn main() -> ExitCode {