Optimizations and build speedup (#6339)

* Optimizations and build speedup

With this commit I have changed several components to be more efficient.
This can be less llvm-lines generated or less `clone()` calls.

 ### Config
- Re-ordered the `make_config` macro to be more efficient
- Created a custom Deserializer for `ConfigBuilder` less code and more efficient
- Use struct's for the `prepare_json` function instead of generating a custom JSON object.
  This generates less code and is more efficient.
- Updated the `get_support_string` function to handle the masking differently.
  This generates less code and also was able to remove some sub-macro-calls

 ### Error
- Added an extra new call to prevent duplicate Strings in generated macro code.
  This generated less llvm-lines and seems to be more efficient.
- Created a custom Serializer for `ApiError` and `CompactApiError`
  This makes that struct smaller in size, so better for memory, but also less llvm-lines.

 ### General
- Removed `once_lock` and replace it all with Rust's std LazyLock
- Added and fixed some Clippy lints which reduced `clone()` calls for example.
- Updated build profiles for more efficiency
  Also added a new profile specifically for CI, which should decrease the build check
- Updated several GitHub Workflows for better security and use the new `ci` build profile
- Updated to Rust v1.90.0 which uses a new linker `rust-lld` which should help in faster building
- Updated the Cargo.toml for all crates to better use the `workspace` variables
- Added a `typos` Workflow and Pre-Commit, which should help in detecting spell error's.
  Also fixed a few found by it.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix release profile

Signed-off-by: BlackDex <black.dex@gmail.com>

* Update typos and remove mimalloc check from pre-commit checks

Signed-off-by: BlackDex <black.dex@gmail.com>

* Misc fixes and updated typos

Signed-off-by: BlackDex <black.dex@gmail.com>

* Update crates and workflows

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix formating and pre-commit

Signed-off-by: BlackDex <black.dex@gmail.com>

* Update to Rust v1.91 and update crates

Signed-off-by: BlackDex <black.dex@gmail.com>

* Update web-vault to v2025.10.1 and xx to v1.8.0

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:
Mathijs van Veluw
2025-11-01 22:21:04 +01:00
committed by GitHub
parent 8d30285160
commit 9017ca265a
47 changed files with 762 additions and 488 deletions

View File

@@ -1,5 +1,6 @@
use std::{
env::consts::EXE_SUFFIX,
fmt,
process::exit,
sync::{
atomic::{AtomicBool, Ordering},
@@ -8,15 +9,15 @@ use std::{
};
use job_scheduler_ng::Schedule;
use once_cell::sync::Lazy;
use reqwest::Url;
use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor};
use crate::{
error::Error,
util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags},
};
static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
static CONFIG_FILE: LazyLock<String> = LazyLock::new(|| {
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{data_folder}/config.json"))
});
@@ -33,7 +34,7 @@ static CONFIG_FILENAME: LazyLock<String> = LazyLock::new(|| {
pub static SKIP_CONFIG_VALIDATION: AtomicBool = AtomicBool::new(false);
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| {
std::thread::spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap_or_else(|e| {
println!("Error loading config:\n {e:?}\n");
@@ -55,6 +56,41 @@ pub static CONFIG: Lazy<Config> = Lazy::new(|| {
pub type Pass = String;
macro_rules! make_config {
// Support string print
( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value(&$value.as_ref().map(|_| String::from("***"))).unwrap() }; // Optional pass, we map to an Option<String> with "***"
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // Required pass, we return "***"
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value(&$value).unwrap() }; // Optional other or string, we convert to json
( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { $value.as_str().into() }; // Required string value, we convert to json
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to json
// Group or empty string
( @show ) => { "" };
( @show $lit:literal ) => { $lit };
// Wrap the optionals in an Option type
( @type $ty:ty, option) => { Option<$ty> };
( @type $ty:ty, $id:ident) => { $ty };
// Generate the values depending on none_action
( @build $value:expr, $config:expr, option, ) => { $value };
( @build $value:expr, $config:expr, def, $default:expr ) => { $value.unwrap_or($default) };
( @build $value:expr, $config:expr, auto, $default_fn:expr ) => {{
match $value {
Some(v) => v,
None => {
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}
}
}};
( @build $value:expr, $config:expr, generated, $default_fn:expr ) => {{
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}};
( @getenv $name:expr, bool ) => { get_env_bool($name) };
( @getenv $name:expr, $ty:ident ) => { get_env($name) };
($(
$(#[doc = $groupdoc:literal])?
$group:ident $(: $group_enabled:ident)? {
@@ -74,10 +110,103 @@ macro_rules! make_config {
_env: ConfigBuilder,
_usr: ConfigBuilder,
_overrides: Vec<String>,
_overrides: Vec<&'static str>,
}
#[derive(Clone, Default, Deserialize, Serialize)]
// Custom Deserialize for ConfigBuilder, mainly based upon https://serde.rs/deserialize-struct.html
// This deserialize doesn't care if there are keys missing, or if there are duplicate keys
// In case of duplicate keys (which should never be possible unless manually edited), the last value is used!
// Main reason for this is removing the `visit_seq` function, which causes a lot of code generation not needed or used for this struct.
impl<'de> Deserialize<'de> for ConfigBuilder {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
const FIELDS: &[&str] = &[
$($(
stringify!($name),
)+)+
];
#[allow(non_camel_case_types)]
enum Field {
$($(
$name,
)+)+
__ignore,
}
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl Visitor<'_> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("ConfigBuilder field identifier")
}
#[inline]
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: de::Error,
{
match value {
$($(
stringify!($name) => Ok(Field::$name),
)+)+
_ => Ok(Field::__ignore),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct ConfigBuilderVisitor;
impl<'de> Visitor<'de> for ConfigBuilderVisitor {
type Value = ConfigBuilder;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("struct ConfigBuilder")
}
#[inline]
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut builder = ConfigBuilder::default();
while let Some(key) = map.next_key()? {
match key {
$($(
Field::$name => {
if builder.$name.is_some() {
return Err(de::Error::duplicate_field(stringify!($name)));
}
builder.$name = map.next_value()?;
}
)+)+
Field::__ignore => {
let _ = map.next_value::<de::IgnoredAny>()?;
}
}
}
Ok(builder)
}
}
deserializer.deserialize_struct("ConfigBuilder", FIELDS, ConfigBuilderVisitor)
}
}
#[derive(Clone, Default, Serialize)]
pub struct ConfigBuilder {
$($(
#[serde(skip_serializing_if = "Option::is_none")]
@@ -86,7 +215,6 @@ macro_rules! make_config {
}
impl ConfigBuilder {
#[allow(clippy::field_reassign_with_default)]
fn from_env() -> Self {
let env_file = get_env("ENV_FILE").unwrap_or_else(|| String::from(".env"));
match dotenvy::from_path(&env_file) {
@@ -148,14 +276,14 @@ macro_rules! make_config {
/// Merges the values of both builders into a new builder.
/// If both have the same element, `other` wins.
fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<String>) -> Self {
fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<&str>) -> Self {
let mut builder = self.clone();
$($(
if let v @Some(_) = &other.$name {
builder.$name = v.clone();
if self.$name.is_some() {
overrides.push(pastey::paste!(stringify!([<$name:upper>])).into());
overrides.push(pastey::paste!(stringify!([<$name:upper>])));
}
}
)+)+
@@ -196,6 +324,32 @@ macro_rules! make_config {
#[derive(Clone, Default)]
struct ConfigItems { $($( $name: make_config! {@type $ty, $none_action}, )+)+ }
#[derive(Serialize)]
struct ElementDoc {
name: &'static str,
description: &'static str,
}
#[derive(Serialize)]
struct ElementData {
editable: bool,
name: &'static str,
value: serde_json::Value,
default: serde_json::Value,
#[serde(rename = "type")]
r#type: &'static str,
doc: ElementDoc,
overridden: bool,
}
#[derive(Serialize)]
pub struct GroupData {
group: &'static str,
grouptoggle: &'static str,
groupdoc: &'static str,
elements: Vec<ElementData>,
}
#[allow(unused)]
impl Config {
$($(
@@ -207,11 +361,12 @@ macro_rules! make_config {
pub fn prepare_json(&self) -> serde_json::Value {
let (def, cfg, overridden) = {
// Lock the inner as short as possible and clone what is needed to prevent deadlocks
let inner = &self.inner.read().unwrap();
(inner._env.build(), inner.config.clone(), inner._overrides.clone())
};
fn _get_form_type(rust_type: &str) -> &'static str {
fn _get_form_type(rust_type: &'static str) -> &'static str {
match rust_type {
"Pass" => "password",
"String" => "text",
@@ -220,48 +375,36 @@ macro_rules! make_config {
}
}
fn _get_doc(doc: &str) -> serde_json::Value {
let mut split = doc.split("|>").map(str::trim);
// We do not use the json!() macro here since that causes a lot of macro recursion.
// This slows down compile time and it also causes issues with rust-analyzer
serde_json::Value::Object({
let mut doc_json = serde_json::Map::new();
doc_json.insert("name".into(), serde_json::to_value(split.next()).unwrap());
doc_json.insert("description".into(), serde_json::to_value(split.next()).unwrap());
doc_json
})
fn _get_doc(doc_str: &'static str) -> ElementDoc {
let mut split = doc_str.split("|>").map(str::trim);
ElementDoc {
name: split.next().unwrap_or_default(),
description: split.next().unwrap_or_default(),
}
}
// We do not use the json!() macro here since that causes a lot of macro recursion.
// This slows down compile time and it also causes issues with rust-analyzer
serde_json::Value::Array(<[_]>::into_vec(Box::new([
$(
serde_json::Value::Object({
let mut group = serde_json::Map::new();
group.insert("group".into(), (stringify!($group)).into());
group.insert("grouptoggle".into(), (stringify!($($group_enabled)?)).into());
group.insert("groupdoc".into(), (make_config! { @show $($groupdoc)? }).into());
let data: Vec<GroupData> = vec![
$( // This repetition is for each group
GroupData {
group: stringify!($group),
grouptoggle: stringify!($($group_enabled)?),
groupdoc: (make_config! { @show $($groupdoc)? }),
group.insert("elements".into(), serde_json::Value::Array(<[_]>::into_vec(Box::new([
$(
serde_json::Value::Object({
let mut element = serde_json::Map::new();
element.insert("editable".into(), ($editable).into());
element.insert("name".into(), (stringify!($name)).into());
element.insert("value".into(), serde_json::to_value(cfg.$name).unwrap());
element.insert("default".into(), serde_json::to_value(def.$name).unwrap());
element.insert("type".into(), (_get_form_type(stringify!($ty))).into());
element.insert("doc".into(), (_get_doc(concat!($($doc),+))).into());
element.insert("overridden".into(), (overridden.contains(&pastey::paste!(stringify!([<$name:upper>])).into())).into());
element
}),
)+
]))));
group
}),
)+
])))
elements: vec![
$( // This repetition is for each element within a group
ElementData {
editable: $editable,
name: stringify!($name),
value: serde_json::to_value(&cfg.$name).unwrap_or_default(),
default: serde_json::to_value(&def.$name).unwrap_or_default(),
r#type: _get_form_type(stringify!($ty)),
doc: _get_doc(concat!($($doc),+)),
overridden: overridden.contains(&pastey::paste!(stringify!([<$name:upper>]))),
},
)+], // End of elements repetition
},
)+]; // End of groups repetition
serde_json::to_value(data).unwrap()
}
pub fn get_support_json(&self) -> serde_json::Value {
@@ -269,8 +412,8 @@ macro_rules! make_config {
// Pass types will always be masked and no need to put them in the list.
// Besides Pass, only String types will be masked via _privacy_mask.
const PRIVACY_CONFIG: &[&str] = &[
"allowed_iframe_ancestors",
"allowed_connect_src",
"allowed_iframe_ancestors",
"database_url",
"domain_origin",
"domain_path",
@@ -278,16 +421,18 @@ macro_rules! make_config {
"helo_name",
"org_creation_users",
"signups_domains_whitelist",
"_smtp_img_src",
"smtp_from_name",
"smtp_from",
"smtp_host",
"smtp_username",
"_smtp_img_src",
"sso_client_id",
"sso_authority",
"sso_callback_path",
"sso_client_id",
];
let cfg = {
// Lock the inner as short as possible and clone what is needed to prevent deadlocks
let inner = &self.inner.read().unwrap();
inner.config.clone()
};
@@ -317,13 +462,21 @@ macro_rules! make_config {
serde_json::Value::Object({
let mut json = serde_json::Map::new();
$($(
json.insert(stringify!($name).into(), make_config! { @supportstr $name, cfg.$name, $ty, $none_action });
json.insert(String::from(stringify!($name)), make_config! { @supportstr $name, cfg.$name, $ty, $none_action });
)+)+;
// Loop through all privacy sensitive keys and mask them
for mask_key in PRIVACY_CONFIG {
if let Some(value) = json.get_mut(*mask_key) {
if let Some(s) = value.as_str() {
*value = _privacy_mask(s).into();
}
}
}
json
})
}
pub fn get_overrides(&self) -> Vec<String> {
pub fn get_overrides(&self) -> Vec<&'static str> {
let overrides = {
let inner = &self.inner.read().unwrap();
inner._overrides.clone()
@@ -332,55 +485,6 @@ macro_rules! make_config {
}
}
};
// Support string print
( @supportstr $name:ident, $value:expr, Pass, option ) => { serde_json::to_value($value.as_ref().map(|_| String::from("***"))).unwrap() }; // Optional pass, we map to an Option<String> with "***"
( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { "***".into() }; // Required pass, we return "***"
( @supportstr $name:ident, $value:expr, String, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config
if PRIVACY_CONFIG.contains(&stringify!($name)) {
serde_json::to_value($value.as_ref().map(|x| _privacy_mask(x) )).unwrap()
} else {
serde_json::to_value($value).unwrap()
}
};
( @supportstr $name:ident, $value:expr, String, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config
if PRIVACY_CONFIG.contains(&stringify!($name)) {
_privacy_mask(&$value).into()
} else {
($value).into()
}
};
( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { serde_json::to_value($value).unwrap() }; // Optional other value, we return as is or convert to string to apply the privacy config
( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { ($value).into() }; // Required other value, we return as is or convert to string to apply the privacy config
// Group or empty string
( @show ) => { "" };
( @show $lit:literal ) => { $lit };
// Wrap the optionals in an Option type
( @type $ty:ty, option) => { Option<$ty> };
( @type $ty:ty, $id:ident) => { $ty };
// Generate the values depending on none_action
( @build $value:expr, $config:expr, option, ) => { $value };
( @build $value:expr, $config:expr, def, $default:expr ) => { $value.unwrap_or($default) };
( @build $value:expr, $config:expr, auto, $default_fn:expr ) => {{
match $value {
Some(v) => v,
None => {
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}
}
}};
( @build $value:expr, $config:expr, generated, $default_fn:expr ) => {{
let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config)
}};
( @getenv $name:expr, bool ) => { get_env_bool($name) };
( @getenv $name:expr, $ty:ident ) => { get_env($name) };
}
//STRUCTURE:
@@ -1518,7 +1622,7 @@ impl Config {
if let Some(akey) = self._duo_akey() {
akey
} else {
let akey_s = crate::crypto::encode_random_bytes::<64>(data_encoding::BASE64);
let akey_s = crate::crypto::encode_random_bytes::<64>(&data_encoding::BASE64);
// Save the new value
let builder = ConfigBuilder {
@@ -1542,7 +1646,7 @@ impl Config {
token.is_some() && !token.unwrap().trim().is_empty()
}
pub fn opendal_operator_for_path_type(&self, path_type: PathType) -> Result<opendal::Operator, Error> {
pub fn opendal_operator_for_path_type(&self, path_type: &PathType) -> Result<opendal::Operator, Error> {
let path = match path_type {
PathType::Data => self.data_folder(),
PathType::IconCache => self.icon_cache_folder(),
@@ -1735,7 +1839,7 @@ fn to_json<'reg, 'rc>(
// Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static WEB_VAULT_VERSION: Lazy<semver::Version> = Lazy::new(|| {
static WEB_VAULT_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {
let vault_version = get_web_vault_version();
// Use a single regex capture to extract version components
let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap();
@@ -1751,7 +1855,7 @@ static WEB_VAULT_VERSION: Lazy<semver::Version> = Lazy::new(|| {
// Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then.
// The default is based upon the version since this feature is added.
static VW_VERSION: Lazy<semver::Version> = Lazy::new(|| {
static VW_VERSION: LazyLock<semver::Version> = LazyLock::new(|| {
let vw_version = crate::VERSION.unwrap_or("1.32.5");
// Use a single regex capture to extract version components
let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap();