fix: smaller fixes from the project review
- settings: one invalid value in ui.json no longer resets all settings (including the storage location), only that value - sync: a worker whose pass outlives a stop/start ends afterwards - updater: only install packages whose signed file name carries a newer version than the running one - canvas: grow the stroke vertex buffer at most up to the device limit Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
a22f081115
commit
e95b03e316
5 files changed
+160
-11
No files matched your search
+29
-2
@@ -1021,8 +1021,13 @@ impl CanvasState {
|
||||
}
|
||||
|
||||
if page.vertex_cache.len() > self.committed_vertex_capacity {
|
||||
self.committed_vertex_capacity =
|
||||
(page.vertex_cache.len() * 2).max(Self::INITIAL_COMMITTED_CAPACITY);
|
||||
let vertex_size = std::mem::size_of::<InkVertex>() as u64;
|
||||
let max_vertices = (self.device.limits().max_buffer_size / vertex_size) as usize;
|
||||
self.committed_vertex_capacity = grown_vertex_capacity(
|
||||
page.vertex_cache.len(),
|
||||
Self::INITIAL_COMMITTED_CAPACITY,
|
||||
max_vertices,
|
||||
);
|
||||
self.committed_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Committed Strokes Vertex Buffer (Reallocated)"),
|
||||
size: (self.committed_vertex_capacity * std::mem::size_of::<InkVertex>()) as u64,
|
||||
@@ -1735,10 +1740,32 @@ fn text_resolution(screen_px_per_unit: f32) -> f32 {
|
||||
2.0_f32.powf(steps / 4.0).clamp(0.125, 8.0)
|
||||
}
|
||||
|
||||
/// Capacity for a vertex buffer that has to hold `needed` vertices: twice as much, so that
|
||||
/// it is not reallocated with every stroke, but no more than the device allows. Doubling
|
||||
/// alone asked for a buffer beyond the limit as soon as a very full page used half of it,
|
||||
/// which ends in a validation panic.
|
||||
fn grown_vertex_capacity(needed: usize, initial: usize, max_vertices: usize) -> usize {
|
||||
needed
|
||||
.saturating_mul(2)
|
||||
.max(initial)
|
||||
.min(max_vertices)
|
||||
.max(needed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vertex_capacity_doubles_up_to_the_device_limit() {
|
||||
assert_eq!(grown_vertex_capacity(10, 1000, 1_000_000), 1000);
|
||||
assert_eq!(grown_vertex_capacity(5000, 1000, 1_000_000), 10_000);
|
||||
// More than half the limit: the limit, not twice the need
|
||||
assert_eq!(grown_vertex_capacity(600_000, 1000, 1_000_000), 1_000_000);
|
||||
// Never less than what is needed
|
||||
assert_eq!(grown_vertex_capacity(2_000_000, 1000, 1_000_000), 2_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_resolution_is_quantized_and_never_below_screen() {
|
||||
for px in [0.2_f32, 0.27, 0.5, 1.3, 3.9] {
|
||||
|
||||
+42
-6
@@ -953,6 +953,16 @@ struct Shared {
|
||||
/// Finished sign-in, to be stored by the app
|
||||
signed_in: Option<Account>,
|
||||
stop: bool,
|
||||
/// Counts the started workers. A worker whose pass was still running when the next one
|
||||
/// was started must end afterwards, although `stop` is `false` again by then.
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
/// Is the worker of `generation` to end?
|
||||
fn retired(&self, generation: u64) -> bool {
|
||||
self.stop || self.generation != generation
|
||||
}
|
||||
}
|
||||
|
||||
type SharedState = Arc<(Mutex<Shared>, Condvar)>;
|
||||
@@ -986,6 +996,7 @@ impl CloudSync {
|
||||
changed: false,
|
||||
signed_in: None,
|
||||
stop: false,
|
||||
generation: 0,
|
||||
}),
|
||||
Condvar::new(),
|
||||
)),
|
||||
@@ -1009,15 +1020,19 @@ impl CloudSync {
|
||||
|
||||
fn start(&mut self, account: Account) {
|
||||
self.stop_worker();
|
||||
{
|
||||
let generation = {
|
||||
let mut shared = lock(&self.shared);
|
||||
shared.stop = false;
|
||||
shared.generation += 1;
|
||||
shared.status = Status::Idle { last: None };
|
||||
shared.next_pass = Instant::now();
|
||||
}
|
||||
shared.generation
|
||||
};
|
||||
let shared = self.shared.clone();
|
||||
let worker_account = account.clone();
|
||||
self.worker = Some(std::thread::spawn(move || worker(shared, worker_account)));
|
||||
self.worker = Some(std::thread::spawn(move || {
|
||||
worker(shared, worker_account, generation)
|
||||
}));
|
||||
self.account = Some(account);
|
||||
}
|
||||
|
||||
@@ -1211,7 +1226,7 @@ fn sign_in(
|
||||
}
|
||||
}
|
||||
|
||||
fn worker(shared: SharedState, account: Account) {
|
||||
fn worker(shared: SharedState, account: Account, generation: u64) {
|
||||
let Some(state_path) = SyncState::file_path() else {
|
||||
set_status(&shared, Status::Failed(t!("Kein Datenordner").to_string()));
|
||||
return;
|
||||
@@ -1228,7 +1243,7 @@ fn worker(shared: SharedState, account: Account) {
|
||||
let (dir, ctx) = {
|
||||
let mut guard = lock(&shared);
|
||||
loop {
|
||||
if guard.stop {
|
||||
if guard.retired(generation) {
|
||||
return;
|
||||
}
|
||||
let now = Instant::now();
|
||||
@@ -1252,7 +1267,7 @@ fn worker(shared: SharedState, account: Account) {
|
||||
eprintln!("[Nextcloud] Sync-Stand nicht gespeichert: {err}");
|
||||
}
|
||||
let mut guard = lock(&shared);
|
||||
if guard.stop {
|
||||
if guard.retired(generation) {
|
||||
return;
|
||||
}
|
||||
guard.status = match result {
|
||||
@@ -1282,6 +1297,27 @@ mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn worker_of_an_earlier_start_is_retired() {
|
||||
let mut shared = Shared {
|
||||
status: Status::Off,
|
||||
local_dir: PathBuf::new(),
|
||||
ctx: Context::default(),
|
||||
next_pass: Instant::now(),
|
||||
changed: false,
|
||||
signed_in: None,
|
||||
stop: false,
|
||||
generation: 1,
|
||||
};
|
||||
assert!(!shared.retired(1));
|
||||
// Stopped and started again while the pass of worker 1 was still running
|
||||
shared.generation = 2;
|
||||
assert!(shared.retired(1));
|
||||
assert!(!shared.retired(2));
|
||||
shared.stop = true;
|
||||
assert!(shared.retired(2));
|
||||
}
|
||||
|
||||
/// Server folder in memory; folder ETags change with anything inside, like Nextcloud's.
|
||||
#[derive(Default)]
|
||||
struct FakeRemote {
|
||||
|
||||
@@ -179,6 +179,10 @@ pub const EN: &[(&str, &str)] = &[
|
||||
("Kariert", "Grid"),
|
||||
("KAROS, LINIEN UND PUNKTE", "GRID, LINES AND DOTS"),
|
||||
("kein Asset-Ordner", "no asset folder"),
|
||||
(
|
||||
"'{asset_name}' ist keine neuere Version und wird nicht installiert.",
|
||||
"'{asset_name}' is not a newer version and will not be installed.",
|
||||
),
|
||||
("Kein Eintrag für '{asset_name}' in SHA256SUMS.txt gefunden. Heruntergeladene Datei wurde gelöscht.", "No entry for '{asset_name}' found in SHA256SUMS.txt. The downloaded file was deleted."),
|
||||
("Kein Release ausgewählt. Download abgelehnt.", "No release selected. Download refused."),
|
||||
("Keine manifest.json vorhanden", "No manifest.json present"),
|
||||
|
||||
+44
-3
@@ -302,15 +302,35 @@ impl UiSettings {
|
||||
crate::paths::config_dir().map(|d| d.join("ui.json"))
|
||||
}
|
||||
|
||||
/// Lädt die Einstellungen; bei fehlender oder beschädigter Datei gelten Standardwerte.
|
||||
/// Keeps every setting of a partly invalid file that can still be read; only the
|
||||
/// unreadable ones get their default. One bad value must not reset everything, least of
|
||||
/// all the storage location.
|
||||
fn salvage(content: &str) -> Self {
|
||||
let Ok(serde_json::Value::Object(file)) = serde_json::from_str(content) else {
|
||||
return Self::default();
|
||||
};
|
||||
let mut good = serde_json::Map::new();
|
||||
for (key, value) in file {
|
||||
good.insert(key.clone(), value);
|
||||
let candidate = serde_json::Value::Object(good.clone());
|
||||
if serde_json::from_value::<Self>(candidate).is_err() {
|
||||
eprintln!("[Einstellungen] '{key}' ist ungültig, verwende Standardwert.");
|
||||
good.remove(&key);
|
||||
}
|
||||
}
|
||||
serde_json::from_value(serde_json::Value::Object(good)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Lädt die Einstellungen; bei fehlender Datei gelten Standardwerte, bei einer teilweise
|
||||
/// ungültigen Datei nur für die unlesbaren Werte.
|
||||
pub fn load_from(path: &Path) -> Self {
|
||||
let mut settings: Self = match std::fs::read_to_string(path) {
|
||||
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|e| {
|
||||
eprintln!(
|
||||
"[Einstellungen] '{}' ist ungültig ({e}), verwende Standardwerte.",
|
||||
"[Einstellungen] '{}' ist ungültig ({e}), unlesbare Werte werden ersetzt.",
|
||||
path.display()
|
||||
);
|
||||
Self::default()
|
||||
Self::salvage(&content)
|
||||
}),
|
||||
Err(_) => Self::default(),
|
||||
};
|
||||
@@ -437,6 +457,27 @@ mod tests {
|
||||
assert_eq!(UiSettings::load_from(&path), UiSettings::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_one_invalid_value_does_not_reset_the_other_settings() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("ui.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
br#"{"storage_dir": "/home/pia/Nextcloud/ink-notes", "eraser_radius_mm": "big", "math_assist": true}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let loaded = UiSettings::load_from(&path);
|
||||
assert_eq!(
|
||||
loaded.storage_dir,
|
||||
Some(PathBuf::from("/home/pia/Nextcloud/ink-notes"))
|
||||
);
|
||||
assert!(loaded.math_assist);
|
||||
assert_eq!(
|
||||
loaded.eraser_radius_mm,
|
||||
UiSettings::default().eraser_radius_mm
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_seven_pens_stay_readable_for_older_versions() {
|
||||
// Versions before 0.44 read `pen_presets` as exactly four pens (Issue #56)
|
||||
|
||||
@@ -272,6 +272,22 @@ pub fn is_newer_version(remote_tag: &str, current_tag: &str, include_prereleases
|
||||
}
|
||||
}
|
||||
|
||||
/// Version in a release file name (`ink-notes_<version>_<arch>.<ext>`).
|
||||
pub fn asset_version(asset_name: &str) -> Option<&str> {
|
||||
let rest = asset_name.strip_prefix("ink-notes_")?;
|
||||
let (version, _) = rest.split_once('_')?;
|
||||
parse_semver(version).map(|_| version)
|
||||
}
|
||||
|
||||
/// Is the package named `asset_name` newer than the running version `current`?
|
||||
///
|
||||
/// The signature only covers file names and hashes, not the release they are offered in. A
|
||||
/// server could therefore present an old, correctly signed package under a new version
|
||||
/// number. The version in the signed file name closes that gap.
|
||||
pub fn asset_is_upgrade(asset_name: &str, current: &str) -> bool {
|
||||
asset_version(asset_name).is_some_and(|version| is_newer_version(version, current, true))
|
||||
}
|
||||
|
||||
/// Berechnet den hex-kodierten SHA-256 Hash einer Datei.
|
||||
pub fn compute_sha256(path: &Path) -> Result<String, String> {
|
||||
let mut file = std::fs::File::open(path).map_err(|e| {
|
||||
@@ -712,6 +728,14 @@ impl UpdaterController {
|
||||
}
|
||||
};
|
||||
|
||||
if !asset_is_upgrade(&safe_asset_name, env!("CARGO_PKG_VERSION")) {
|
||||
self.status = UpdaterStatus::Error(t!(
|
||||
"'{asset_name}' ist keine neuere Version und wird nicht installiert.",
|
||||
asset_name = safe_asset_name
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
self.status = UpdaterStatus::Downloading {
|
||||
asset_name: safe_asset_name.clone(),
|
||||
percent: 0.0,
|
||||
@@ -1036,6 +1060,23 @@ mod tests {
|
||||
assert!(is_newer_version("1.0.0-rc1", "0.9.0", true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_packages_named_as_newer_are_upgrades() {
|
||||
assert_eq!(asset_version("ink-notes_0.46.0_amd64.deb"), Some("0.46.0"));
|
||||
assert_eq!(
|
||||
asset_version("ink-notes_0.46.0_x86_64.AppImage"),
|
||||
Some("0.46.0")
|
||||
);
|
||||
assert_eq!(asset_version("SHA256SUMS.txt"), None);
|
||||
assert_eq!(asset_version("ink-notes_latest_amd64.deb"), None);
|
||||
|
||||
assert!(asset_is_upgrade("ink-notes_0.46.0_arm64.apk", "0.45.0"));
|
||||
// An old release offered again under a new tag
|
||||
assert!(!asset_is_upgrade("ink-notes_0.30.0_amd64.deb", "0.45.0"));
|
||||
assert!(!asset_is_upgrade("ink-notes_0.45.0_x86_64.exe", "0.45.0"));
|
||||
assert!(!asset_is_upgrade("something_else.deb", "0.45.0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_asset_name() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user