Skip to main content

max / balanced_breakfast

Clear the clippy backlog so BB passes its release gate Forty-two warnings across bb-core, bb-feed and the desktop crate, most of them collapsible_if that edition 2024 let-chains fold flat. BB was the only one of the eight Bento repos failing the prebuild gate. Two of clippy's suggestions were not taken as offered. json_to_i32 became i32::from(*b) rather than a guarded match arm that leans on fallthrough for the false case. The bookmark excerpt keeps its comment on the map() call instead of stranding it above a blank line. The rewrites that moved a condition into a match guard (atom summary/published, the query-feed operators) all fall through to a no-op arm when the guard fails, so they read differently but decide the same. 611 tests pass; clippy -D warnings is clean with the release features. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 04:29 UTC
Commit: 9638766ea23a5a844115a430457ec268d59e37fb
Parent: ffbd7ab
14 files changed, +249 insertions, -269 deletions
@@ -104,7 +104,7 @@ pub fn load_or_create_key_from_keychain(file_path: &Path) -> Result<EncryptionKe
104 104 if file_path.exists() {
105 105 let key = load_or_create_key(file_path)?;
106 106 // Migrate to keychain
107 - let b64 = BASE64.encode(&*key);
107 + let b64 = BASE64.encode(*key);
108 108 if entry.set_password(&b64).is_ok() {
109 109 // Read back and verify before deleting the file fallback
110 110 if let Ok(readback) = entry.get_password() {
@@ -138,7 +138,7 @@ pub fn load_or_create_key_from_keychain(file_path: &Path) -> Result<EncryptionKe
138 138 and will be flagged for re-entry."
139 139 );
140 140 let key = generate_key();
141 - let b64 = BASE64.encode(&*key);
141 + let b64 = BASE64.encode(*key);
142 142 if entry.set_password(&b64).is_ok() {
143 143 return Ok(key);
144 144 }
@@ -213,15 +213,16 @@ pub fn encrypt_config_secrets(
213 213 if field.field_type != ConfigFieldType::Secret {
214 214 continue;
215 215 }
216 - if let Some(serde_json::Value::String(val)) = obj.get(&field.key) {
217 - if !val.is_empty() && !val.starts_with(PREFIX) {
218 - match encrypt_field(val, key) {
219 - Ok(encrypted) => {
220 - obj.insert(field.key.clone(), serde_json::Value::String(encrypted));
221 - }
222 - Err(e) => {
223 - tracing::warn!(field = %field.key, error = %e, "Failed to encrypt secret, plaintext retained");
224 - }
216 + if let Some(serde_json::Value::String(val)) = obj.get(&field.key)
217 + && !val.is_empty()
218 + && !val.starts_with(PREFIX)
219 + {
220 + match encrypt_field(val, key) {
221 + Ok(encrypted) => {
222 + obj.insert(field.key.clone(), serde_json::Value::String(encrypted));
223 + }
224 + Err(e) => {
225 + tracing::warn!(field = %field.key, error = %e, "Failed to encrypt secret, plaintext retained");
225 226 }
226 227 }
227 228 }
@@ -249,17 +250,17 @@ pub fn decrypt_config_secrets(
249 250 if field.field_type != ConfigFieldType::Secret {
250 251 continue;
251 252 }
252 - if let Some(serde_json::Value::String(val)) = obj.get(&field.key) {
253 - if val.starts_with(PREFIX) {
254 - match decrypt_field(val, key) {
255 - Ok(decrypted) => {
256 - obj.insert(field.key.clone(), serde_json::Value::String(decrypted));
257 - }
258 - Err(e) => {
259 - tracing::error!(field = %field.key, error = %e, "Failed to decrypt secret, clearing field to prevent ciphertext leakage. Prompting for re-entry.");
260 - obj.insert(field.key.clone(), serde_json::Value::String(String::new()));
261 - needs_reentry.push(field.key.clone());
262 - }
253 + if let Some(serde_json::Value::String(val)) = obj.get(&field.key)
254 + && val.starts_with(PREFIX)
255 + {
256 + match decrypt_field(val, key) {
257 + Ok(decrypted) => {
258 + obj.insert(field.key.clone(), serde_json::Value::String(decrypted));
259 + }
260 + Err(e) => {
261 + tracing::error!(field = %field.key, error = %e, "Failed to decrypt secret, clearing field to prevent ciphertext leakage. Prompting for re-entry.");
262 + obj.insert(field.key.clone(), serde_json::Value::String(String::new()));
263 + needs_reentry.push(field.key.clone());
263 264 }
264 265 }
265 266 }
@@ -32,72 +32,72 @@ pub(crate) fn dynamic_to_config_schema(val: Dynamic) -> Result<ConfigSchema, Rha
32 32
33 33 let mut schema = ConfigSchema::new(description);
34 34
35 - if let Some(fields_val) = map.get("fields") {
36 - if let Some(fields) = fields_val.clone().try_cast::<rhai::Array>() {
37 - for field_val in fields {
38 - if let Some(field_map) = field_val.try_cast::<Map>() {
39 - let key = field_map
40 - .get("key")
41 - .and_then(|v| v.clone().try_cast::<String>())
42 - .unwrap_or_default();
43 -
44 - let label = field_map
45 - .get("label")
46 - .and_then(|v| v.clone().try_cast::<String>())
47 - .unwrap_or_default();
48 -
49 - let field_type_str = field_map
50 - .get("field_type")
51 - .and_then(|v| v.clone().try_cast::<String>())
52 - .unwrap_or_else(|| "text".to_string());
53 -
54 - let field_type = match field_type_str.to_lowercase().as_str() {
55 - "url" => ConfigFieldType::Url,
56 - "secret" => ConfigFieldType::Secret,
57 - "textarea" => ConfigFieldType::TextArea,
58 - "number" => ConfigFieldType::Number,
59 - "toggle" => ConfigFieldType::Toggle,
60 - "select" => ConfigFieldType::Select,
61 - _ => ConfigFieldType::Text,
62 - };
63 -
64 - let required = field_map
65 - .get("required")
66 - .and_then(|v| v.clone().try_cast::<bool>())
67 - .unwrap_or(false);
68 -
69 - // Try both "default" and "default_value" since "default" is reserved in Rhai
70 - let default_val = field_map
71 - .get("default_value")
72 - .or_else(|| field_map.get("default"))
73 - .and_then(|v| v.clone().try_cast::<String>());
74 -
75 - let mut field = ConfigField {
76 - key,
77 - label,
78 - field_type,
79 - required,
80 - description: field_map
81 - .get("description")
82 - .and_then(|v| v.clone().try_cast::<String>()),
83 - default: default_val,
84 - placeholder: field_map
85 - .get("placeholder")
86 - .and_then(|v| v.clone().try_cast::<String>()),
87 - options: Vec::new(),
88 - };
89 -
90 - if let Some(opts_val) = field_map.get("options") {
91 - if let Some(opts) = opts_val.clone().try_cast::<rhai::Array>() {
92 - field.options = opts
93 - .into_iter()
94 - .filter_map(|v| v.try_cast::<String>())
95 - .collect();
96 - }
97 - }
98 -
99 - schema.fields.push(field);
35 + if let Some(fields_val) = map.get("fields")
36 + && let Some(fields) = fields_val.clone().try_cast::<rhai::Array>()
37 + {
38 + for field_val in fields {
39 + if let Some(field_map) = field_val.try_cast::<Map>() {
40 + let key = field_map
41 + .get("key")
42 + .and_then(|v| v.clone().try_cast::<String>())
43 + .unwrap_or_default();
44 +
45 + let label = field_map
46 + .get("label")
47 + .and_then(|v| v.clone().try_cast::<String>())
48 + .unwrap_or_default();
49 +
50 + let field_type_str = field_map
51 + .get("field_type")
52 + .and_then(|v| v.clone().try_cast::<String>())
53 + .unwrap_or_else(|| "text".to_string());
54 +
55 + let field_type = match field_type_str.to_lowercase().as_str() {
56 + "url" => ConfigFieldType::Url,
57 + "secret" => ConfigFieldType::Secret,
58 + "textarea" => ConfigFieldType::TextArea,
59 + "number" => ConfigFieldType::Number,
60 + "toggle" => ConfigFieldType::Toggle,
61 + "select" => ConfigFieldType::Select,
62 + _ => ConfigFieldType::Text,
63 + };
64 +
65 + let required = field_map
66 + .get("required")
67 + .and_then(|v| v.clone().try_cast::<bool>())
68 + .unwrap_or(false);
69 +
70 + // Try both "default" and "default_value" since "default" is reserved in Rhai
71 + let default_val = field_map
72 + .get("default_value")
73 + .or_else(|| field_map.get("default"))
74 + .and_then(|v| v.clone().try_cast::<String>());
75 +
76 + let mut field = ConfigField {
77 + key,
78 + label,
79 + field_type,
80 + required,
81 + description: field_map
82 + .get("description")
83 + .and_then(|v| v.clone().try_cast::<String>()),
84 + default: default_val,
85 + placeholder: field_map
86 + .get("placeholder")
87 + .and_then(|v| v.clone().try_cast::<String>()),
88 + options: Vec::new(),
89 + };
90 +
91 + if let Some(opts_val) = field_map.get("options")
92 + && let Some(opts) = opts_val.clone().try_cast::<rhai::Array>()
93 + {
94 + field.options = opts
95 + .into_iter()
96 + .filter_map(|v| v.try_cast::<String>())
97 + .collect();
100 98 }
99 +
100 + schema.fields.push(field);
101 101 }
102 102 }
103 103 }
@@ -267,29 +267,29 @@ pub(crate) fn dynamic_to_feed_item(
267 267 .get("url")
268 268 .and_then(|v| v.clone().try_cast::<String>());
269 269
270 - if let Some(media_val) = content_map.get("media") {
271 - if let Some(media_arr) = media_val.clone().try_cast::<rhai::Array>() {
272 - content.media = media_arr
273 - .into_iter()
274 - .filter_map(|v| v.try_cast::<String>())
275 - .collect();
276 - }
270 + if let Some(media_val) = content_map.get("media")
271 + && let Some(media_arr) = media_val.clone().try_cast::<rhai::Array>()
272 + {
273 + content.media = media_arr
274 + .into_iter()
275 + .filter_map(|v| v.try_cast::<String>())
276 + .collect();
277 277 }
278 278
279 - if let Some(actions_val) = content_map.get("actions") {
280 - if let Some(actions_arr) = actions_val.clone().try_cast::<rhai::Array>() {
281 - content.actions = actions_arr
282 - .into_iter()
283 - .filter_map(|v| {
284 - let m = v.try_cast::<Map>()?;
285 - Some(ItemAction {
286 - label: m.get("label")?.clone().try_cast::<String>()?,
287 - action_type: m.get("action_type")?.clone().try_cast::<String>()?,
288 - url: m.get("url")?.clone().try_cast::<String>()?,
289 - })
279 + if let Some(actions_val) = content_map.get("actions")
280 + && let Some(actions_arr) = actions_val.clone().try_cast::<rhai::Array>()
281 + {
282 + content.actions = actions_arr
283 + .into_iter()
284 + .filter_map(|v| {
285 + let m = v.try_cast::<Map>()?;
286 + Some(ItemAction {
287 + label: m.get("label")?.clone().try_cast::<String>()?,
288 + action_type: m.get("action_type")?.clone().try_cast::<String>()?,
289 + url: m.get("url")?.clone().try_cast::<String>()?,
290 290 })
291 - .collect();
292 - }
291 + })
292 + .collect();
293 293 }
294 294 content
295 295 } else {
@@ -317,13 +317,13 @@ pub(crate) fn dynamic_to_feed_item(
317 317 .get("score")
318 318 .and_then(|v| v.clone().try_cast::<i64>());
319 319
320 - if let Some(tags_val) = meta_map.get("tags") {
321 - if let Some(tags_arr) = tags_val.clone().try_cast::<rhai::Array>() {
322 - meta.tags = tags_arr
323 - .into_iter()
324 - .filter_map(|v| v.try_cast::<String>())
325 - .collect();
326 - }
320 + if let Some(tags_val) = meta_map.get("tags")
321 + && let Some(tags_arr) = tags_val.clone().try_cast::<rhai::Array>()
322 + {
323 + meta.tags = tags_arr
324 + .into_iter()
325 + .filter_map(|v| v.try_cast::<String>())
326 + .collect();
327 327 }
328 328 meta
329 329 } else {
@@ -118,22 +118,18 @@ fn parse_atom_entry(node: &roxmltree::Node) -> Dynamic {
118 118 "title" => {
119 119 entry.insert("title".into(), get_text_content(&child).into());
120 120 }
121 - "summary" | "content" => {
122 - if !entry.contains_key("summary") {
123 - entry.insert("summary".into(), get_text_content(&child).into());
124 - }
121 + "summary" | "content" if !entry.contains_key("summary") => {
122 + entry.insert("summary".into(), get_text_content(&child).into());
125 123 }
126 124 "link" => {
127 125 if let Some(href) = child.attribute("href") {
128 126 entry.insert("link".into(), href.to_string().into());
129 127 }
130 128 }
131 - "published" | "updated" => {
132 - if !entry.contains_key("published") {
133 - let date_str = get_text_content(&child);
134 - if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&date_str) {
135 - entry.insert("published".into(), dt.timestamp().into());
136 - }
129 + "published" | "updated" if !entry.contains_key("published") => {
130 + let date_str = get_text_content(&child);
131 + if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&date_str) {
132 + entry.insert("published".into(), dt.timestamp().into());
137 133 }
138 134 }
139 135 "author" => {
@@ -82,10 +82,10 @@ pub(crate) fn parse_json_feed(json_str: &str) -> Result<Dynamic, Box<rhai::EvalA
82 82 }
83 83
84 84 // Published date
85 - if let Some(date_str) = item_obj.get("date_published").and_then(|v| v.as_str()) {
86 - if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(date_str) {
87 - entry.insert("published".into(), dt.timestamp().into());
88 - }
85 + if let Some(date_str) = item_obj.get("date_published").and_then(|v| v.as_str())
86 + && let Ok(dt) = chrono::DateTime::parse_from_rfc3339(date_str)
87 + {
88 + entry.insert("published".into(), dt.timestamp().into());
89 89 }
90 90
91 91 // Author: JSON Feed 1.1 uses "authors" array, 1.0 uses "author" object
@@ -36,12 +36,11 @@ pub(super) const MAX_FETCH_DURATION: Duration = Duration::from_secs(60);
36 36
37 37 /// Check if a host is in the CGNAT/shared address space (100.64.0.0/10).
38 38 fn is_cgnat(host: &str) -> bool {
39 - if let Some(rest) = host.strip_prefix("100.") {
40 - if let Some(second) = rest.split('.').next() {
41 - if let Ok(n) = second.parse::<u8>() {
42 - return (64..=127).contains(&n);
43 - }
44 - }
39 + if let Some(rest) = host.strip_prefix("100.")
40 + && let Some(second) = rest.split('.').next()
41 + && let Ok(n) = second.parse::<u8>()
42 + {
43 + return (64..=127).contains(&n);
45 44 }
46 45 false
47 46 }
@@ -74,7 +73,7 @@ fn validate_url(url: &str) -> Result<(), String> {
74 73 };
75 74 let host = host.as_str();
76 75 // Strip userinfo (user@) from host to prevent blocklist bypass via http://user@localhost/
77 - let host = host.split('@').last().unwrap_or(host);
76 + let host = host.split('@').next_back().unwrap_or(host);
78 77 if host == "localhost"
79 78 || host == "127.0.0.1"
80 79 || host == "[::1]"
@@ -99,14 +98,12 @@ fn validate_url(url: &str) -> Result<(), String> {
99 98 }
100 99 }
101 100 // Block 172.16.0.0/12
102 - if let Some(rest) = host.strip_prefix("172.") {
103 - if let Some(second) = rest.split('.').next() {
104 - if let Ok(n) = second.parse::<u8>() {
105 - if (16..=31).contains(&n) {
106 - return Err(format!("Blocked request to internal address: {}", url));
107 - }
108 - }
109 - }
101 + if let Some(rest) = host.strip_prefix("172.")
102 + && let Some(second) = rest.split('.').next()
103 + && let Ok(n) = second.parse::<u8>()
104 + && (16..=31).contains(&n)
105 + {
106 + return Err(format!("Blocked request to internal address: {}", url));
110 107 }
111 108 // Block encoded IP forms: decimal (2130706433), hex (0x7f000001),
112 109 // and octal-prefixed octets (0177.0.0.1). These bypass string-based
@@ -40,7 +40,7 @@ impl OrderBy {
40 40 pub fn apply(&self, items: &mut [FeedItem]) {
41 41 match self {
42 42 OrderBy::Chronological => {
43 - items.sort_by(|a, b| b.meta.published_at.cmp(&a.meta.published_at));
43 + items.sort_by_key(|i| std::cmp::Reverse(i.meta.published_at));
44 44 }
45 45 OrderBy::Score => {
46 46 // Use i64::MIN for None so scoreless items sort after all scored items
@@ -205,10 +205,10 @@ impl FeedFilter {
205 205 /// Check if an item matches using pre-compiled regex cache.
206 206 pub fn matches_with_cache(&self, item: &FeedItem, regex_cache: &HashMap<usize, Regex>) -> bool {
207 207 // Check source filter
208 - if let Some(ref source) = self.source {
209 - if item.id.source.as_str() != source {
210 - return false;
211 - }
208 + if let Some(ref source) = self.source
209 + && item.id.source.as_str() != source
210 + {
211 + return false;
212 212 }
213 213
214 214 // Check unread filter
@@ -266,26 +266,24 @@ impl FeedFilter {
266 266 _ => "",
267 267 };
268 268 match c.operator.as_str() {
269 - "contains" => {
270 - if !field_value.to_lowercase().contains(&c.value.to_lowercase()) {
271 - return false;
272 - }
269 + "contains"
270 + if !field_value.to_lowercase().contains(&c.value.to_lowercase()) =>
271 + {
272 + return false;
273 273 }
274 - "not_contains" => {
275 - if field_value.to_lowercase().contains(&c.value.to_lowercase()) {
276 - return false;
277 - }
274 + "not_contains"
275 + if field_value.to_lowercase().contains(&c.value.to_lowercase()) =>
276 + {
277 + return false;
278 278 }
279 - "equals" => {
280 - if !field_value.eq_ignore_ascii_case(&c.value) {
281 - return false;
282 - }
279 + "equals" if !field_value.eq_ignore_ascii_case(&c.value) => {
280 + return false;
283 281 }
284 282 "matches_regex" => {
285 - if let Some(re) = regex_cache.get(&i) {
286 - if !re.is_match(field_value) {
287 - return false;
288 - }
283 + if let Some(re) = regex_cache.get(&i)
284 + && !re.is_match(field_value)
285 + {
286 + return false;
289 287 }
290 288 // Missing from cache = invalid regex, skip (already warned)
291 289 }
@@ -213,10 +213,10 @@ pub async fn create_bookmark_from_item(
213 213 {
214 214 return Err(ApiError::bad_request("Item is already bookmarked"));
215 215 }
216 - if let Some(ref url) = item.url {
217 - if db.bookmarks().get_by_url(url).await?.is_some() {
218 - return Err(ApiError::bad_request("URL is already bookmarked"));
219 - }
216 + if let Some(ref url) = item.url
217 + && db.bookmarks().get_by_url(url).await?.is_some()
218 + {
219 + return Err(ApiError::bad_request("URL is already bookmarked"));
220 220 }
221 221
222 222 let url = item.url.clone().unwrap_or_default();
@@ -232,11 +232,8 @@ pub async fn create_bookmark_from_item(
232 232 description: item
233 233 .body
234 234 .clone()
235 - .map(|b| {
236 - // Truncate body to a description-length excerpt
237 - let plain = b.chars().take(300).collect::<String>();
238 - plain
239 - })
235 + // Truncate body to a description-length excerpt
236 + .map(|b| b.chars().take(300).collect::<String>())
240 237 .unwrap_or_default(),
241 238 author: item.bite_author.clone(),
242 239 source_name: item.source_name.clone(),
@@ -120,58 +120,58 @@ fn validate_feed_input(
120 120 )));
121 121 }
122 122
123 - if let Some(serde_json::Value::String(s)) = value {
124 - if !s.is_empty() {
125 - match field.field_type {
126 - bb_interface::ConfigFieldType::Url => {
127 - if !s.starts_with("http://") && !s.starts_with("https://") {
128 - return Err(ApiError::bad_request(format!(
129 - "'{}' must start with http:// or https://",
130 - field.label
131 - )));
132 - }
133 - let after_scheme = s.split("://").nth(1).unwrap_or("");
134 - if after_scheme.is_empty() || after_scheme == "/" {
135 - return Err(ApiError::bad_request(format!(
136 - "'{}' is not a valid URL",
137 - field.label
138 - )));
139 - }
123 + if let Some(serde_json::Value::String(s)) = value
124 + && !s.is_empty()
125 + {
126 + match field.field_type {
127 + bb_interface::ConfigFieldType::Url => {
128 + if !s.starts_with("http://") && !s.starts_with("https://") {
129 + return Err(ApiError::bad_request(format!(
130 + "'{}' must start with http:// or https://",
131 + field.label
132 + )));
140 133 }
141 - bb_interface::ConfigFieldType::Number => {
142 - if s.parse::<f64>().is_err() {
143 - return Err(ApiError::bad_request(format!(
144 - "'{}' must be a valid number",
145 - field.label
146 - )));
147 - }
134 + let after_scheme = s.split("://").nth(1).unwrap_or("");
135 + if after_scheme.is_empty() || after_scheme == "/" {
136 + return Err(ApiError::bad_request(format!(
137 + "'{}' is not a valid URL",
138 + field.label
139 + )));
148 140 }
149 - bb_interface::ConfigFieldType::Select => {
150 - if !field.options.contains(s) {
151 - return Err(ApiError::bad_request(format!(
152 - "'{}' must be one of: {}",
153 - field.label,
154 - field.options.join(", ")
155 - )));
156 - }
141 + }
142 + bb_interface::ConfigFieldType::Number => {
143 + if s.parse::<f64>().is_err() {
144 + return Err(ApiError::bad_request(format!(
145 + "'{}' must be a valid number",
146 + field.label
147 + )));
157 148 }
158 - bb_interface::ConfigFieldType::Toggle => {
159 - if s != "true" && s != "false" {
160 - return Err(ApiError::bad_request(format!(
161 - "'{}' must be \"true\" or \"false\"",
162 - field.label
163 - )));
164 - }
149 + }
150 + bb_interface::ConfigFieldType::Select => {
151 + if !field.options.contains(s) {
152 + return Err(ApiError::bad_request(format!(
153 + "'{}' must be one of: {}",
154 + field.label,
155 + field.options.join(", ")
156 + )));
165 157 }
166 - bb_interface::ConfigFieldType::Text
167 - | bb_interface::ConfigFieldType::TextArea
168 - | bb_interface::ConfigFieldType::Secret => {
169 - if s.len() > 10_000 {
170 - return Err(ApiError::bad_request(format!(
171 - "'{}' exceeds maximum length of 10,000 characters",
172 - field.label
173 - )));
174 - }
158 + }
159 + bb_interface::ConfigFieldType::Toggle => {
160 + if s != "true" && s != "false" {
161 + return Err(ApiError::bad_request(format!(
162 + "'{}' must be \"true\" or \"false\"",
163 + field.label
164 + )));
165 + }
166 + }
167 + bb_interface::ConfigFieldType::Text
168 + | bb_interface::ConfigFieldType::TextArea
169 + | bb_interface::ConfigFieldType::Secret => {
170 + if s.len() > 10_000 {
171 + return Err(ApiError::bad_request(format!(
172 + "'{}' exceeds maximum length of 10,000 characters",
173 + field.label
174 + )));
175 175 }
176 176 }
177 177 }
@@ -434,7 +434,7 @@ pub async fn download_and_open(url: String) -> Result<(), ApiError> {
434 434 ));
435 435 }
436 436
437 - let result = tokio::task::spawn_blocking(move || -> Result<(), ApiError> {
437 + tokio::task::spawn_blocking(move || -> Result<(), ApiError> {
438 438 let resp = ureq::get(&url)
439 439 .call()
440 440 .map_err(|e| ApiError::internal(format!("Download failed: {}", e)))?;
@@ -493,9 +493,7 @@ pub async fn download_and_open(url: String) -> Result<(), ApiError> {
493 493 Ok(())
494 494 })
495 495 .await
496 - .map_err(|e| ApiError::internal(format!("Task join error: {}", e)))?;
497 -
498 - result
496 + .map_err(|e| ApiError::internal(format!("Task join error: {}", e)))?
499 497 }
500 498
501 499 #[cfg(test)]
@@ -148,10 +148,10 @@ pub async fn import_opml(
148 148
149 149 // Re-initialize the RSS plugin so it discovers the newly imported feed
150 150 // URLs without requiring an app restart.
151 - if imported > 0 {
152 - if let Err(e) = state.orchestrator.init_plugin_from_db("rss").await {
153 - tracing::warn!(error = %e, "Failed to reinitialize RSS plugin");
154 - }
151 + if imported > 0
152 + && let Err(e) = state.orchestrator.init_plugin_from_db("rss").await
153 + {
154 + tracing::warn!(error = %e, "Failed to reinitialize RSS plugin");
155 155 }
156 156
157 157 info!(
@@ -112,6 +112,7 @@ enum StoredCallback {
112 112 /// Returns the port. The server handles:
113 113 /// - The browser redirect with `?code=...&state=...` (stores result, returns HTML)
114 114 /// - `/result` polling endpoint (returns JSON: pending, success, or error)
115 + ///
115 116 /// Any previously running callback server is cancelled via the shared generation counter.
116 117 fn start_callback_server() -> Result<u16, ApiError> {
117 118 let listener = std::net::TcpListener::bind("127.0.0.1:0")
@@ -123,10 +123,10 @@ impl AppState {
123 123
124 124 // Clean up old download temp files from previous sessions
125 125 let downloads_dir = std::env::temp_dir().join("bb-downloads");
126 - if downloads_dir.exists() {
127 - if let Err(e) = std::fs::remove_dir_all(&downloads_dir) {
128 - debug!(error = %e, "Failed to clean up old download temp files");
129 - }
126 + if downloads_dir.exists()
127 + && let Err(e) = std::fs::remove_dir_all(&downloads_dir)
128 + {
129 + debug!(error = %e, "Failed to clean up old download temp files");
130 130 }
131 131
132 132 info!("Application state initialized");
@@ -265,10 +265,10 @@ fn find_plugins_dir(app: &AppHandle) -> PathBuf {
265 265 .parent()
266 266 .map(|p| p.join("plugins"));
267 267
268 - if let Some(ref dev_path) = dev_plugins {
269 - if dev_path.exists() {
270 - return dev_path.clone();
271 - }
268 + if let Some(ref dev_path) = dev_plugins
269 + && dev_path.exists()
270 + {
271 + return dev_path.clone();
272 272 }
273 273
274 274 // In production, use the app config dir (fall back to app data dir)
@@ -296,14 +296,12 @@ fn copy_bundled_plugins(app: &AppHandle, plugins_dir: &PathBuf) {
296 296 if path.extension().is_some_and(|e| e == "rhai") {
297 297 let dest = plugins_dir.join(entry.file_name());
298 298 // Skip if installed file is identical to bundled version
299 - if dest.exists() {
300 - if let (Ok(bundled_bytes), Ok(installed_bytes)) =
299 + if dest.exists()
300 + && let (Ok(bundled_bytes), Ok(installed_bytes)) =
301 301 (std::fs::read(&path), std::fs::read(&dest))
302 - {
303 - if bundled_bytes == installed_bytes {
304 - continue;
305 - }
306 - }
302 + && bundled_bytes == installed_bytes
303 + {
304 + continue;
307 305 }
308 306 if let Err(e) = std::fs::copy(&path, &dest) {
309 307 tracing::warn!(error = %e, ?path, "Failed to copy plugin");
@@ -133,23 +133,23 @@ pub fn start_sync_scheduler(app: AppHandle) -> tokio::task::AbortHandle {
133 133 }
134 134 };
135 135
136 - if !last_sync.is_empty() {
137 - if let Ok(last) = chrono::DateTime::parse_from_rfc3339(&last_sync) {
138 - let elapsed = chrono::Utc::now() - last.with_timezone(&chrono::Utc);
139 - if elapsed.num_minutes() < interval_minutes as i64 {
140 - continue;
141 - }
136 + if !last_sync.is_empty()
137 + && let Ok(last) = chrono::DateTime::parse_from_rfc3339(&last_sync)
138 + {
139 + let elapsed = chrono::Utc::now() - last.with_timezone(&chrono::Utc);
140 + if elapsed.num_minutes() < interval_minutes as i64 {
141 + continue;
142 142 }
143 143 }
144 144 }
145 145 sse_triggered = false;
146 146
147 147 // Check backoff
148 - if let Some(until) = backoff_until {
149 - if chrono::Utc::now() < until {
150 - debug!(%until, "Sync scheduler: backing off");
151 - continue;
152 - }
148 + if let Some(until) = backoff_until
149 + && chrono::Utc::now() < until
150 + {
151 + debug!(%until, "Sync scheduler: backing off");
152 + continue;
153 153 }
154 154
155 155 // Prevent concurrent sync operations (manual + scheduler).
@@ -101,10 +101,10 @@ async fn apply_changes_inner_tx(
101 101
102 102 for table in UPSERT_ORDER {
103 103 for change in &upserts {
104 - if change.table == *table {
105 - if let Some(ref data) = change.data {
106 - apply_upsert_exec(&mut **tx, table, &change.row_id, data).await?;
107 - }
104 + if change.table == *table
105 + && let Some(ref data) = change.data
106 + {
107 + apply_upsert_exec(tx, table, &change.row_id, data).await?;
108 108 }
109 109 }
110 110 }
@@ -112,7 +112,7 @@ async fn apply_changes_inner_tx(
112 112 for table in DELETE_ORDER {
113 113 for change in &deletes {
114 114 if change.table == *table {
115 - apply_delete_exec(&mut **tx, table, &change.row_id).await?;
115 + apply_delete_exec(tx, table, &change.row_id).await?;
116 116 }
117 117 }
118 118 }
@@ -179,7 +179,7 @@ async fn apply_upsert(
179 179 data: &serde_json::Value,
180 180 ) -> Result<(), ApiError> {
181 181 let mut conn = pool.acquire().await.map_err(super::db_err)?;
182 - apply_upsert_exec(&mut *conn, table, row_id, data).await
182 + apply_upsert_exec(&mut conn, table, row_id, data).await
183 183 }
184 184
185 185 /// Apply an INSERT OR REPLACE for a remote change on any SQLite executor.
@@ -286,13 +286,7 @@ async fn apply_upsert_exec(
286 286 fn json_to_i32(val: &serde_json::Value) -> i32 {
287 287 match val {
288 288 serde_json::Value::Number(n) => n.as_i64().unwrap_or(0) as i32,
289 - serde_json::Value::Bool(b) => {
290 - if *b {
291 - 1
292 - } else {
293 - 0
294 - }
295 - }
289 + serde_json::Value::Bool(b) => i32::from(*b),
296 290 _ => 0,
297 291 }
298 292 }
@@ -301,7 +295,7 @@ fn json_to_i32(val: &serde_json::Value) -> i32 {
301 295 #[cfg(test)]
302 296 async fn apply_delete(pool: &SqlitePool, table: &str, row_id: &str) -> Result<(), ApiError> {
303 297 let mut conn = pool.acquire().await.map_err(super::db_err)?;
304 - apply_delete_exec(&mut *conn, table, row_id).await
298 + apply_delete_exec(&mut conn, table, row_id).await
305 299 }
306 300
307 301 /// Apply a DELETE for a remote change on any SQLite executor.