fix: drop serde_yaml

This commit is contained in:
zhom committed 2026-10-13 07:49:12 +04:00
1 parent 382db45803
commit d6c21de169
5 files changed
+133 -98

No files matched your search

+17 -20
View File
@@ -252,6 +252,12 @@ dependencies = [
"password-hash",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -1787,7 +1793,6 @@ dependencies = [
"security-framework",
"serde",
"serde_json",
"serde_yaml",
"serial_test",
"sha2 0.11.0",
"shadowsocks",
@@ -1825,6 +1830,7 @@ dependencies = [
"windows",
"winreg",
"wiremock",
"yaml-rust2",
"zip",
]
@@ -5920,19 +5926,6 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.14.2",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "serial_test"
version = "4.0.1"
@@ -7614,12 +7607,6 @@ dependencies = [
"ctutils",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -8680,6 +8667,16 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]]
name = "yaml-rust2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57e5b818a27a4cd30884ea380857a5e56f7ec3ba24a3990a3cc0b95af3238e18"
dependencies = [
"arraydeque",
"hashlink",
]
[[package]]
name = "yoke"
version = "0.8.3"
+1 -1
View File
@@ -113,7 +113,7 @@ async-socks5 = "0.6"
# Wayfern CDP integration
tokio-tungstenite = { version = "0.30", features = ["native-tls"] }
rusqlite = { version = "0.40", features = ["bundled"] }
serde_yaml = "0.9"
yaml-rust2 = { version = "0.13", default-features = false }
thiserror = "2.0"
regex-lite = "0.1"
tempfile = "3"
+95 -48
View File
@@ -32,6 +32,8 @@ use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use toml_edit::{DocumentMut, Item, Table};
use yaml_rust2::yaml::Hash as YamlMap;
use yaml_rust2::{Yaml, YamlEmitter, YamlLoader};
pub const SERVER_NAME: &str = "donut-browser";
const REMOTE_MCP_PATH: &str = "/api/mcp";
@@ -745,14 +747,67 @@ fn toml_item_to_json(item: &Item) -> serde_json::Value {
}
}
fn entry_to_yaml(entry: &Entry) -> Result<serde_yaml::Value, String> {
let mut mapping = serde_yaml::Mapping::new();
for (key, value) in entry {
let yaml =
serde_yaml::to_value(value).map_err(|e| format!("Failed to convert to YAML: {e}"))?;
mapping.insert(serde_yaml::Value::String(key.to_string()), yaml);
fn json_to_yaml(value: &serde_json::Value) -> Yaml {
match value {
serde_json::Value::Null => Yaml::Null,
serde_json::Value::Bool(b) => Yaml::Boolean(*b),
serde_json::Value::Number(n) => match n.as_i64() {
Some(i) => Yaml::Integer(i),
None => Yaml::Real(n.to_string()),
},
serde_json::Value::String(s) => Yaml::String(s.clone()),
serde_json::Value::Array(items) => Yaml::Array(items.iter().map(json_to_yaml).collect()),
serde_json::Value::Object(map) => Yaml::Hash(
map
.iter()
.map(|(k, v)| (Yaml::String(k.clone()), json_to_yaml(v)))
.collect(),
),
}
}
/// A YAML value as plain JSON, for inspection. Keys that are not strings and
/// values JSON cannot hold (aliases, bad values) read as null.
fn yaml_to_json(value: &Yaml) -> serde_json::Value {
match value {
Yaml::Boolean(b) => serde_json::Value::Bool(*b),
Yaml::Integer(i) => serde_json::Value::from(*i),
Yaml::Real(text) => text
.parse::<f64>()
.ok()
.and_then(serde_json::Number::from_f64)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
Yaml::String(s) => serde_json::Value::String(s.clone()),
Yaml::Array(items) => serde_json::Value::Array(items.iter().map(yaml_to_json).collect()),
Yaml::Hash(map) => serde_json::Value::Object(
map
.iter()
.filter_map(|(k, v)| Some((k.as_str()?.to_string(), yaml_to_json(v))))
.collect(),
),
_ => serde_json::Value::Null,
}
}
fn entry_to_yaml(entry: &Entry) -> Yaml {
Yaml::Hash(
entry
.iter()
.map(|(key, value)| (Yaml::String(key.to_string()), json_to_yaml(value)))
.collect(),
)
}
/// The mapping at `key` in a YAML mapping, if it is one.
fn yaml_child<'a>(root: &'a mut Yaml, key: &str) -> Option<&'a mut YamlMap> {
match root {
Yaml::Hash(map) => match map.get_mut(&Yaml::String(key.to_string())) {
Some(Yaml::Hash(child)) => Some(child),
_ => None,
},
_ => None,
}
Ok(serde_yaml::Value::Mapping(mapping))
}
/// A parsed config file that can be edited without disturbing what the user
@@ -760,7 +815,7 @@ fn entry_to_yaml(entry: &Entry) -> Result<serde_yaml::Value, String> {
enum Document {
Json(CstRootNode),
Toml(DocumentMut),
Yaml(serde_yaml::Value),
Yaml(Yaml),
}
impl Document {
@@ -771,12 +826,11 @@ impl Document {
),
ConfigFormat::Toml => Self::Toml(content.parse::<DocumentMut>().map_err(|e| e.to_string())?),
ConfigFormat::Yaml => {
let value = if content.trim().is_empty() {
serde_yaml::Value::Null
} else {
serde_yaml::from_str(content).map_err(|e| e.to_string())?
};
Self::Yaml(value)
let mut documents = YamlLoader::load_from_str(content).map_err(|e| e.to_string())?;
if documents.len() > 1 {
return Err("the file holds more than one YAML document".to_string());
}
Self::Yaml(documents.pop().unwrap_or(Yaml::Null))
}
})
}
@@ -816,20 +870,13 @@ impl Document {
.collect()
})
.unwrap_or_default(),
Self::Yaml(root) => root
.get(key)
.and_then(serde_yaml::Value::as_mapping)
.map(|servers| {
servers
.iter()
.filter_map(|(name, value)| {
let name = name.as_str()?.to_string();
let value = serde_json::to_value(value).ok()?;
Some((name, value))
})
.collect()
})
.unwrap_or_default(),
Self::Yaml(root) => match &root[key] {
Yaml::Hash(servers) => servers
.iter()
.filter_map(|(name, value)| Some((name.as_str()?.to_string(), yaml_to_json(value))))
.collect(),
_ => Vec::new(),
},
}
}
@@ -869,24 +916,21 @@ impl Document {
}
Self::Yaml(root) => {
if root.is_null() {
*root = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
*root = Yaml::Hash(YamlMap::new());
}
let root_map = root
.as_mapping_mut()
.ok_or("the file's top level is not a YAML mapping")?;
let Yaml::Hash(root_map) = root else {
return Err("the file's top level is not a YAML mapping".to_string());
};
let servers = root_map
.entry(serde_yaml::Value::String(key.to_string()))
.or_insert_with(|| serde_yaml::Value::Mapping(serde_yaml::Mapping::new()));
.entry(Yaml::String(key.to_string()))
.or_insert_with(|| Yaml::Hash(YamlMap::new()));
if servers.is_null() {
*servers = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
*servers = Yaml::Hash(YamlMap::new());
}
servers
.as_mapping_mut()
.ok_or_else(|| format!("\"{key}\" is not a YAML mapping"))?
.insert(
serde_yaml::Value::String(SERVER_NAME.to_string()),
entry_to_yaml(entry)?,
);
let Yaml::Hash(servers) = servers else {
return Err(format!("\"{key}\" is not a YAML mapping"));
};
servers.insert(Yaml::String(SERVER_NAME.to_string()), entry_to_yaml(entry));
}
}
Ok(())
@@ -911,12 +955,9 @@ impl Document {
}
}
Self::Yaml(root) => {
if let Some(servers) = root
.get_mut(key)
.and_then(serde_yaml::Value::as_mapping_mut)
{
if let Some(servers) = yaml_child(root, key) {
for name in names {
servers.remove(name.as_str());
servers.remove(&Yaml::String(name.clone()));
}
}
}
@@ -928,7 +969,13 @@ impl Document {
Self::Json(root) => Ok(root.to_string()),
Self::Toml(doc) => Ok(doc.to_string()),
Self::Yaml(root) => {
serde_yaml::to_string(root).map_err(|e| format!("Failed to serialize YAML: {e}"))
let mut text = String::new();
YamlEmitter::new(&mut text)
.dump(root)
.map_err(|e| format!("Failed to serialize YAML: {e}"))?;
// The emitter opens with a document marker the file never had.
let body = text.strip_prefix("---\n").unwrap_or(&text);
Ok(format!("{body}\n"))
}
}
}
+12 -21
View File
@@ -15,6 +15,7 @@ import {
LuTriangleAlert,
LuUser,
} from "react-icons/lu";
import { AccountSyncServer } from "@/components/account-sync-server";
import {
formatDate,
formatHours,
@@ -136,12 +137,11 @@ export function AccountPage({
useState<ConnectionStatus>("unknown");
const hasConfig = Boolean(serverUrl && token);
// Self-hosted and cloud are mutually exclusive — both share the same sync
// engine and a profile can't be sync'd to two backends. The tab trigger is
// disabled here AND the backend rejects mixed state (see `save_sync_settings`
// / `cloud_logout`), so even if someone bypasses the UI we don't end up
// with split-brain.
const selfHostedDisabled = isLoggedIn || isCloudLoading;
// Signed out, a server takes its shared token. Signed in, the account signs
// in to its own server instead (`AccountSyncServer`); the backend refuses a
// shared token then (`save_sync_settings`), so one device never syncs one
// profile to two places.
const selfHostedDisabled = isCloudLoading;
const handleRefresh = async () => {
setIsRefreshing(true);
@@ -358,11 +358,6 @@ export function AccountPage({
<AnimatedTabsTrigger
value="self-hosted"
disabled={selfHostedDisabled}
title={
selfHostedDisabled
? t("account.selfHosted.disabledWhileLoggedIn")
: undefined
}
>
{t("account.tabs.selfHosted")}
</AnimatedTabsTrigger>
@@ -582,16 +577,12 @@ export function AccountPage({
value="self-hosted"
className="@container mt-4"
>
{selfHostedDisabled ? (
// Defensive: the tab trigger is disabled while the user is
// logged in, so this branch shouldn't be reachable via UI —
// but if state flips mid-render (e.g. a cloud login finishes
// while the tab is open), show the explanation instead of
// a silent empty card.
<p className="text-sm text-muted-foreground">
{t("account.selfHosted.disabledWhileLoggedIn")}
</p>
) : (
{isLoggedIn && user ? (
<AccountSyncServer
paid={getEntitlements(user).cloudBackup}
teamSeat={Boolean(user.teamId)}
/>
) : selfHostedDisabled ? null : (
<SettingsPanel className="flex flex-col gap-4">
<div className="flex items-start gap-3">
<FeatureIcon active={connectionStatus === "connected"}>
+8 -8
View File
@@ -451,6 +451,10 @@
"name": "aria-hidden",
"license": "MIT"
},
{
"name": "arraydeque",
"license": "Apache-2.0 OR MIT"
},
{
"name": "arrayref",
"license": "BSD-2-Clause"
@@ -2519,10 +2523,6 @@
"name": "serde_with_macros",
"license": "Apache-2.0 OR MIT"
},
{
"name": "serde_yaml",
"license": "Apache-2.0 OR MIT"
},
{
"name": "serialize-to-javascript",
"license": "Apache-2.0 OR MIT"
@@ -2983,10 +2983,6 @@
"name": "universal-hash",
"license": "Apache-2.0 OR MIT"
},
{
"name": "unsafe-libyaml",
"license": "MIT"
},
{
"name": "untrusted",
"license": "ISC"
@@ -3263,6 +3259,10 @@
"name": "y4m",
"license": "MIT"
},
{
"name": "yaml-rust2",
"license": "Apache-2.0 OR MIT"
},
{
"name": "yoke",
"license": "Unicode-3.0"