From bda7cda59d5446940ab2cf6d75d9bac0ceb39822 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Thu, 21 Dec 2023 18:09:37 +0100 Subject: [PATCH] Updated installer --- Cargo.lock | 19 +- crates/cli/Cargo.toml | 2 +- crates/imap/Cargo.toml | 2 +- crates/imap/src/core/client.rs | 4 +- crates/imap/src/core/writer.rs | 12 +- crates/install/Cargo.toml | 3 +- crates/install/src/main.rs | 528 +++++++++++------- crates/jmap/Cargo.toml | 2 +- crates/jmap/src/mailbox/query.rs | 1 - crates/main/Cargo.toml | 2 +- crates/managesieve/Cargo.toml | 2 +- crates/nlp/Cargo.toml | 2 +- crates/smtp/Cargo.toml | 2 +- crates/store/src/backend/foundationdb/read.rs | 16 +- .../store/src/backend/foundationdb/write.rs | 33 +- crates/store/src/backend/mysql/read.rs | 12 +- crates/store/src/backend/mysql/write.rs | 16 +- crates/store/src/backend/postgres/read.rs | 12 +- crates/store/src/backend/postgres/write.rs | 16 +- crates/store/src/backend/rocksdb/read.rs | 43 +- crates/store/src/backend/rocksdb/write.rs | 31 +- crates/store/src/backend/sqlite/read.rs | 12 +- crates/store/src/backend/sqlite/write.rs | 14 +- crates/store/src/dispatch/store.rs | 2 +- crates/store/src/lib.rs | 6 +- crates/store/src/query/filter.rs | 2 +- crates/store/src/write/key.rs | 45 +- crates/utils/Cargo.toml | 2 +- resources/config.zip | Bin 165065 -> 171238 bytes resources/config/config.toml | 4 +- .../{elastic.toml => elasticsearch.toml} | 6 +- .../config/store/{filesystem.toml => fs.toml} | 0 tests/src/jmap/mod.rs | 4 +- 33 files changed, 510 insertions(+), 347 deletions(-) rename resources/config/store/{elastic.toml => elasticsearch.toml} (80%) rename resources/config/store/{filesystem.toml => fs.toml} (100%) diff --git a/Cargo.lock b/Cargo.lock index d52747ca..3761885c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2548,7 +2548,7 @@ checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" [[package]] name = "imap" -version = "0.4.2" +version = "0.5.0" dependencies = [ "ahash 0.8.6", "dashmap", @@ -2725,7 +2725,7 @@ dependencies = [ [[package]] name = "jmap" -version = "0.4.2" +version = "0.5.0" dependencies = [ "aes", "aes-gcm", @@ -3129,7 +3129,7 @@ dependencies = [ [[package]] name = "mail-server" -version = "0.4.2" +version = "0.5.0" dependencies = [ "directory", "imap", @@ -3146,7 +3146,7 @@ dependencies = [ [[package]] name = "managesieve" -version = "0.4.2" +version = "0.5.0" dependencies = [ "ahash 0.8.6", "bincode", @@ -3422,7 +3422,7 @@ dependencies = [ [[package]] name = "nlp" -version = "0.4.2" +version = "0.5.0" dependencies = [ "ahash 0.8.6", "bincode", @@ -5361,7 +5361,7 @@ checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "smtp" -version = "0.4.2" +version = "0.5.0" dependencies = [ "ahash 0.8.6", "bincode", @@ -5482,7 +5482,7 @@ dependencies = [ [[package]] name = "stalwart-cli" -version = "0.4.2" +version = "0.5.0" dependencies = [ "clap", "console", @@ -5506,7 +5506,7 @@ dependencies = [ [[package]] name = "stalwart-install" -version = "0.4.2" +version = "0.5.0" dependencies = [ "base64 0.21.5", "clap", @@ -5519,7 +5519,6 @@ dependencies = [ "rand", "reqwest", "rpassword", - "rusqlite", "tar", "zip-extract", ] @@ -6419,7 +6418,7 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "utils" -version = "0.4.2" +version = "0.5.0" dependencies = [ "ahash 0.8.6", "chrono", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index daa64ae8..c0fb83f1 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. "] license = "AGPL-3.0-only" repository = "https://github.com/stalwartlabs/cli" homepage = "https://github.com/stalwartlabs/cli" -version = "0.4.2" +version = "0.5.0" edition = "2021" readme = "README.md" resolver = "2" diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 79305da7..01c7b1ef 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "0.4.2" +version = "0.5.0" edition = "2021" resolver = "2" diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index e7fec781..9fbeb4ef 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -36,9 +36,9 @@ use super::{SelectedMailbox, Session, SessionData, State, IMAP}; impl Session { pub async fn ingest(&mut self, bytes: &[u8]) -> crate::Result { - for line in String::from_utf8_lossy(bytes).split("\r\n") { + /*for line in String::from_utf8_lossy(bytes).split("\r\n") { let c = println!("{}", line); - } + }*/ tracing::trace!(parent: &self.span, event = "read", diff --git a/crates/imap/src/core/writer.rs b/crates/imap/src/core/writer.rs index e61df02d..68ac1404 100644 --- a/crates/imap/src/core/writer.rs +++ b/crates/imap/src/core/writer.rs @@ -58,7 +58,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender { @@ -101,7 +101,7 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender { @@ -131,9 +131,9 @@ pub fn spawn_writer(mut stream: Event, span: tracing::Span) -> mpsc::Sender Session { pub async fn write_bytes(&self, bytes: impl Into>) -> crate::OpResult { let bytes = bytes.into(); - for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { + /*for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { let c = println!("{}", line); - } + }*/ if let Err(err) = self.writer.send(Event::Bytes(bytes)).await { debug!("Failed to send bytes: {}", err); @@ -147,9 +147,9 @@ impl Session { impl SessionData { pub async fn write_bytes(&self, bytes: impl Into>) -> bool { let bytes = bytes.into(); - for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { + /*for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { let c = println!("{}", line); - } + }*/ if let Err(err) = self.writer.send(Event::Bytes(bytes)).await { debug!("Failed to send bytes: {}", err); diff --git a/crates/install/Cargo.toml b/crates/install/Cargo.toml index e8c2e0a9..22f79a8f 100644 --- a/crates/install/Cargo.toml +++ b/crates/install/Cargo.toml @@ -5,14 +5,13 @@ authors = ["Stalwart Labs Ltd. "] license = "AGPL-3.0-only" repository = "https://github.com/stalwartlabs/mail-server" homepage = "https://github.com/stalwartlabs/mail-server" -version = "0.4.2" +version = "0.5.0" edition = "2021" readme = "README.md" resolver = "2" [dependencies] reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots", "blocking"] } -rusqlite = { version = "0.30.0", features = ["bundled"] } rpassword = "7.0" indicatif = "0.17.0" dialoguer = "0.11" diff --git a/crates/install/src/main.rs b/crates/install/src/main.rs index 48557674..445af062 100644 --- a/crates/install/src/main.rs +++ b/crates/install/src/main.rs @@ -22,6 +22,7 @@ */ use std::{ + fmt::{Display, Formatter}, fs, io::Cursor, path::{Path, PathBuf}, @@ -32,9 +33,7 @@ use base64::{engine::general_purpose, Engine}; use clap::{Parser, ValueEnum}; use dialoguer::{console::Term, theme::ColorfulTheme, Input, Select}; use openssl::rsa::Rsa; -use pwhash::sha512_crypt; use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use rusqlite::{Connection, OpenFlags}; const CONFIG_URL: &str = "https://get.stalw.art/resources/config.zip"; @@ -65,30 +64,47 @@ enum Component { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Backend { - SQLite, +enum Store { + RocksDB, FoundationDB, + SQLite, + PostgreSQL, + MySQL, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Blob { - Local, - MinIO, + Internal, + Filesystem, S3, - Gcs, - Azure, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Fts { + Internal, + ElasticSearch, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SpamDb { + Internal, + Redis, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Directory { - Sql, + Internal, Ldap, - None, + PostgreSQL, + MySQL, + SQLite, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SmtpDirectory { - Sql, + PostgreSQL, + MySQL, + SQLite, Ldap, Lmtp, Imap, @@ -173,17 +189,20 @@ fn main() -> std::io::Result<()> { let mut download_url = None; // Obtain database engine - let directory = if component != Component::Smtp { - if !skip_download { - let backend = select::( - "Which database engine would you like to use?", - &[ - "SQLite (single node, replicated with Litestream)", - "FoundationDB (distributed and fault-tolerant)", - ], - Backend::SQLite, - )?; + if component != Component::Smtp { + let backend = select::( + "Which database would you like to use?", + &[ + "RocksDB (recommended for single-node setups)", + "FoundationDB (recommended for distributed environments)", + "SQLite", + "PostgreSQL", + "MySQL", + ], + Store::RocksDB, + )?; + if !skip_download { download_url = format!( concat!( "https://github.com/stalwartlabs/{}", @@ -202,46 +221,70 @@ fn main() -> std::io::Result<()> { Component::Smtp => unreachable!(), }, match backend { - Backend::SQLite => "sqlite", - Backend::FoundationDB => "foundationdb", + Store::FoundationDB => "distributed", + _ => "local", }, TARGET, PKG_EXTENSION ) .into(); } + let store = backend.to_string(); let blob = select::( - "Where would you like to store e-mails and blobs?", + "Where would you like to store e-mails and other large binaries?", &[ - "Local disk using Maildir", - "MinIO (or any S3-compatible object storage)", - "Amazon S3", - "Google Cloud Storage", - "Azure Blob Storage", + &store, + "Local file system", + "S3, MinIO or any S3-compatible object storage", ], - Blob::Local, + Blob::Internal, )?; let directory = select::( - "Do you already have a directory or database containing your accounts?", + "Do you already have a directory or database containing your user accounts?", &[ - "Yes, it's an SQL database", - "Yes, it's an LDAP directory", - "No, create a new directory for me", + &format!("No, I want Stalwart to manage my user accounts in {store}"), + "Yes, it's an LDAP server", + "Yes, it's an PostgreSQL database", + "Yes, it's an MySQL database", + "Yes, it's an SQLite database", ], - Directory::None, + Directory::Internal, + )?; + + let fts = select::( + "Where would you like to store the full-text index?", + &[&store, "ElasticSearch"], + Fts::Internal, + )?; + + let spamdb = select::( + "Where would you like to store the anti-spam database?", + &[&store, "Redis"], + SpamDb::Internal, )?; // Update settings - if blob != Blob::Local { + sed( + cfg_path.join("config.toml"), + &[ + ("__STORE__", backend.id()), + ("__DIRECTORY__", directory.id()), + ], + ); + sed( + cfg_path.join("jmap").join("store.toml"), + &[ + ("__BLOB_STORE__", blob.id().unwrap_or("%{DEFAULT_STORE}%")), + ("__FTS_STORE__", fts.id().unwrap_or("%{DEFAULT_STORE}%")), + ], + ); + if let Some(id) = spamdb.id() { sed( - cfg_path.join("jmap").join("store.toml"), - &[("\"local\"", "\"s3\"")], + cfg_path.join("common").join("sieve.toml"), + &[("%{DEFAULT_STORE}%", id)], ); } - if directory == Directory::Ldap { - sed(cfg_path.join("config.toml"), &[("/sql.toml", "/ldap.toml")]); - } sed( cfg_path.join("jmap").join("oauth.toml"), &[( @@ -254,12 +297,47 @@ fn main() -> std::io::Result<()> { )], ); - directory + // Enable stores + for store in [ + backend.id().into(), + blob.id(), + fts.id(), + spamdb.id(), + directory.sql_store_id(), + ] + .into_iter() + .flatten() + { + sed( + cfg_path.join("store").join(format!("{store}.toml")), + &[("disable = true", "disable = false")], + ); + } + + // Enable directory + if let Some(sql_id) = directory.sql_store_id() { + sed( + cfg_path.join("directory").join("sql.toml"), + &[ + ("disable = true", "disable = false"), + ("__SQL_STORE__", sql_id), + ], + ); + } else { + sed( + cfg_path + .join("directory") + .join(format!("{}.toml", directory.id())), + &[("disable = true", "disable = false")], + ); + } } else { let smtp_directory = select::( "How should your local accounts be validated?", &[ - "SQL database", + "PostgreSQL database", + "MySQL database", + "SQLite database", "LDAP directory", "LMTP server", "IMAP server", @@ -267,34 +345,61 @@ fn main() -> std::io::Result<()> { SmtpDirectory::Lmtp, )?; - if smtp_directory == SmtpDirectory::Ldap { - sed(cfg_path.join("config.toml"), &[("/sql.toml", "/ldap.toml")]); + let spamdb = select::( + "Where would you like to store the anti-spam database?", + &["Local database", "Redis"], + SpamDb::Internal, + )?; + + // Update settings + sed( + cfg_path.join("config.toml"), + &[ + ("__STORE__", "rocksdb"), + ("__DIRECTORY__", smtp_directory.id()), + ], + ); + sed( + cfg_path.join("jmap").join("store.toml"), + &[ + ("__BLOB_STORE__", "%{DEFAULT_STORE}%"), + ("__FTS_STORE__", "%{DEFAULT_STORE}%"), + ], + ); + if let Some(id) = spamdb.id() { + sed( + cfg_path.join("common").join("sieve.toml"), + &[("%{DEFAULT_STORE}%", id)], + ); } - match smtp_directory { - SmtpDirectory::Ldap => { - sed(cfg_path.join("config.toml"), &[("/sql.toml", "/ldap.toml")]); - } - SmtpDirectory::Lmtp | SmtpDirectory::Imap => { - let d_type = if smtp_directory == SmtpDirectory::Lmtp { - "lmtp" - } else { - "imap" - }; - sed( - cfg_path.join("smtp").join("queue.toml"), - &[("default", d_type)], - ); - sed( - cfg_path.join("smtp").join("session.toml"), - &[("default", d_type)], - ); - sed( - cfg_path.join("config.toml"), - &[("\"%{BASE_PATH}%/etc/directory/", format!("\"%{{BASE_PATH}}%/etc/directory/{d_type}.toml\",\n\t\"%{{BASE_PATH}}%/etc/directory/"))], - ); - } - SmtpDirectory::Sql => (), + // Enable directory + if let Some(sql_id) = smtp_directory.sql_store_id() { + sed( + cfg_path.join("directory").join("sql.toml"), + &[ + ("disable = true", "disable = false"), + ("__SQL_STORE__", sql_id), + ], + ); + } else { + sed( + cfg_path + .join("directory") + .join(format!("{}.toml", smtp_directory.id())), + &[("disable = true", "disable = false")], + ); + } + + // Enable stores + for store in [smtp_directory.sql_store_id(), spamdb.id(), "rocksdb".into()] + .into_iter() + .flatten() + { + sed( + cfg_path.join("store").join(format!("{store}.toml")), + &[("disable = true", "disable = false")], + ); } if !skip_download { @@ -307,12 +412,7 @@ fn main() -> std::io::Result<()> { ) .into(); } - match smtp_directory { - SmtpDirectory::Sql => Directory::Sql, - SmtpDirectory::Ldap => Directory::Ldap, - SmtpDirectory::Lmtp | SmtpDirectory::Imap => Directory::None, - } - }; + } // Download binary if let Some(download_url) = download_url { @@ -401,16 +501,6 @@ fn main() -> std::io::Result<()> { // Generate DKIM key and instructions let dkim_instructions = generate_dkim(&base_path, &domain, &hostname)?; - // Create authentication and spam filter SQLite databases - let admin_password = create_databases( - &base_path, - if matches!(directory, Directory::None) { - Some(&domain) - } else { - None - }, - )?; - // Update config file if args.docker { sed( @@ -440,6 +530,7 @@ fn main() -> std::io::Result<()> { ); // Write service file + if !args.docker { // Change permissions #[cfg(not(target_env = "msvc"))] @@ -542,10 +633,6 @@ fn main() -> std::io::Result<()> { eprintln!("\nšŸŽ‰ Installation completed!\n\nāœ… {dkim_instructions}\n"); - if let Some(admin_password) = admin_password { - eprintln!("šŸ”‘ The administrator account is 'admin' with password '{admin_password}'.\n",); - } - Ok(()) } @@ -690,82 +777,6 @@ fn create_directories(path: &Path) -> std::io::Result<()> { Ok(()) } -fn create_databases(base_path: &Path, domain: Option<&str>) -> std::io::Result> { - // Create Spam database - let path = PathBuf::from(base_path) - .join("data") - .join("spamfilter.sqlite3"); - let conn = Connection::open_with_flags(path, OpenFlags::default()).map_err(|err| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to open database: {}", err), - ) - })?; - for query in [ - concat!( - "CREATE TABLE IF NOT EXISTS bayes_tokens (h1 INTEGER NOT NULL, ", - "h2 INTEGER NOT NULL, ws INTEGER, wh INTEGER, PRIMARY KEY (h1, h2))", - ), - concat!( - "CREATE TABLE IF NOT EXISTS seen_ids (id STRING NOT NULL PRIMARY KEY", - ", ttl DATETIME NOT NULL)", - ), - concat!( - "CREATE TABLE IF NOT EXISTS reputation (token STRING NOT NULL PRIMARY KEY", - ", score FLOAT NOT NULL DEFAULT '0', count INT(11) NOT NULL ", - "DEFAULT '0', ttl DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)", - ), - ] { - conn.execute(query, []).map_err(|err| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to create database: {}", err), - ) - })?; - } - - if let Some(domain) = domain { - // Create accounts database - let mut path = PathBuf::from(base_path); - path.push("data"); - if !path.exists() { - fs::create_dir_all(&path)?; - } - path.push("accounts.sqlite3"); - - let conn = Connection::open_with_flags(path, OpenFlags::default()).map_err(|err| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to open database: {}", err), - ) - })?; - let secret = thread_rng() - .sample_iter(Alphanumeric) - .take(12) - .map(char::from) - .collect::(); - let hashed_secret = sha512_crypt::hash(&secret).unwrap(); - for query in [ - concat!("CREATE TABLE IF NOT EXISTS accounts (name TEXT PRIMARY KEY, secret TEXT, description TEXT, ","type TEXT NOT NULL, quota INTEGER DEFAULT 0, active BOOLEAN DEFAULT 1)").to_string(), - concat!("CREATE TABLE IF NOT EXISTS group_members (name TEXT NOT NULL, member_of ","TEXT NOT NULL, PRIMARY KEY (name, member_of))").to_string(), - concat!("CREATE TABLE IF NOT EXISTS emails (name TEXT NOT NULL, address TEXT NOT NULL",", type TEXT, PRIMARY KEY (name, address))").to_string(), - format!("INSERT OR REPLACE INTO accounts (name, secret, description, type) VALUES ('admin', '{hashed_secret}', 'Postmaster', 'admin')"), - format!("INSERT OR REPLACE INTO emails (name, address, type) VALUES ('admin', 'postmaster@{domain}', 'primary')"), - "INSERT OR IGNORE INTO group_members (name, member_of) VALUES ('admin', 'superusers')".to_string() - ] { - conn.execute(&query, []).map_err(|err| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to create database: {}", err), - ) - })?; - } - Ok(Some(secret)) - } else { - Ok(None) - } -} - fn generate_dkim(path: &Path, domain: &str, hostname: &str) -> std::io::Result { let mut path = PathBuf::from(path); path.push("etc"); @@ -857,19 +868,25 @@ impl SelectItem for Component { } } -impl SelectItem for Backend { +impl SelectItem for Store { fn from_index(index: usize) -> Self { match index { - 0 => Self::SQLite, + 0 => Self::RocksDB, 1 => Self::FoundationDB, + 2 => Self::SQLite, + 3 => Self::PostgreSQL, + 4 => Self::MySQL, _ => unreachable!(), } } fn to_index(&self) -> usize { match self { - Self::SQLite => 0, - Self::FoundationDB => 1, + Store::RocksDB => 0, + Store::FoundationDB => 1, + Store::SQLite => 2, + Store::PostgreSQL => 3, + Store::MySQL => 4, } } } @@ -877,18 +894,22 @@ impl SelectItem for Backend { impl SelectItem for Directory { fn from_index(index: usize) -> Self { match index { - 0 => Self::Sql, + 0 => Self::Internal, 1 => Self::Ldap, - 2 => Self::None, + 2 => Self::PostgreSQL, + 3 => Self::MySQL, + 4 => Self::SQLite, _ => unreachable!(), } } fn to_index(&self) -> usize { match self { - Self::Sql => 0, - Self::Ldap => 1, - Self::None => 2, + Directory::Internal => 0, + Directory::Ldap => 1, + Directory::PostgreSQL => 2, + Directory::MySQL => 3, + Directory::SQLite => 4, } } } @@ -896,20 +917,24 @@ impl SelectItem for Directory { impl SelectItem for SmtpDirectory { fn from_index(index: usize) -> Self { match index { - 0 => Self::Sql, - 1 => Self::Ldap, - 2 => Self::Lmtp, - 3 => Self::Imap, + 0 => Self::PostgreSQL, + 1 => Self::MySQL, + 2 => Self::SQLite, + 3 => Self::Ldap, + 4 => Self::Lmtp, + 5 => Self::Imap, _ => unreachable!(), } } fn to_index(&self) -> usize { match self { - SmtpDirectory::Sql => 0, - SmtpDirectory::Ldap => 1, - SmtpDirectory::Lmtp => 2, - SmtpDirectory::Imap => 3, + SmtpDirectory::PostgreSQL => 0, + SmtpDirectory::MySQL => 1, + SmtpDirectory::SQLite => 2, + SmtpDirectory::Ldap => 3, + SmtpDirectory::Lmtp => 4, + SmtpDirectory::Imap => 5, } } } @@ -917,22 +942,52 @@ impl SelectItem for SmtpDirectory { impl SelectItem for Blob { fn from_index(index: usize) -> Self { match index { - 0 => Blob::Local, - 1 => Blob::MinIO, - 2 => Blob::S3, - 3 => Blob::Gcs, - 4 => Blob::Azure, + 0 => Self::Internal, + 1 => Self::Filesystem, + 2 => Self::S3, _ => unreachable!(), } } fn to_index(&self) -> usize { match self { - Blob::Local => 0, - Blob::MinIO => 1, + Blob::Internal => 0, + Blob::Filesystem => 1, Blob::S3 => 2, - Blob::Gcs => 3, - Blob::Azure => 4, + } + } +} + +impl SelectItem for SpamDb { + fn from_index(index: usize) -> Self { + match index { + 0 => Self::Internal, + 1 => Self::Redis, + _ => unreachable!(), + } + } + + fn to_index(&self) -> usize { + match self { + SpamDb::Internal => 0, + SpamDb::Redis => 1, + } + } +} + +impl SelectItem for Fts { + fn from_index(index: usize) -> Self { + match index { + 0 => Self::Internal, + 1 => Self::ElasticSearch, + _ => unreachable!(), + } + } + + fn to_index(&self) -> usize { + match self { + Fts::Internal => 0, + Fts::ElasticSearch => 1, } } } @@ -973,3 +1028,94 @@ impl Component { } } } + +impl Display for Store { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::RocksDB => write!(f, "RocksDB"), + Self::FoundationDB => write!(f, "FoundationDB"), + Self::SQLite => write!(f, "SQLite"), + Self::PostgreSQL => write!(f, "PostgreSQL"), + Self::MySQL => write!(f, "MySQL"), + } + } +} + +impl Store { + pub fn id(&self) -> &'static str { + match self { + Self::RocksDB => "rocksdb", + Self::FoundationDB => "foundationdb", + Self::SQLite => "sqlite", + Self::PostgreSQL => "postgresql", + Self::MySQL => "mysql", + } + } +} + +impl Directory { + pub fn id(&self) -> &'static str { + match self { + Directory::Internal => "internal", + Directory::Ldap => "ldap", + Directory::PostgreSQL | Directory::MySQL | Directory::SQLite => "sql", + } + } + + pub fn sql_store_id(&self) -> Option<&'static str> { + match self { + Directory::PostgreSQL => Some("postgresql"), + Directory::MySQL => Some("mysql"), + Directory::SQLite => Some("sqlite"), + Directory::Internal | Directory::Ldap => None, + } + } +} + +impl SmtpDirectory { + pub fn id(&self) -> &'static str { + match self { + SmtpDirectory::Ldap => "ldap", + SmtpDirectory::Lmtp => "lmtp", + SmtpDirectory::Imap => "imap", + SmtpDirectory::PostgreSQL | SmtpDirectory::MySQL | SmtpDirectory::SQLite => "sql", + } + } + + pub fn sql_store_id(&self) -> Option<&'static str> { + match self { + SmtpDirectory::PostgreSQL => Some("postgresql"), + SmtpDirectory::MySQL => Some("mysql"), + SmtpDirectory::SQLite => Some("sqlite"), + SmtpDirectory::Ldap | SmtpDirectory::Lmtp | SmtpDirectory::Imap => None, + } + } +} + +impl Blob { + pub fn id(&self) -> Option<&'static str> { + match self { + Self::Internal => None, + Self::Filesystem => "fs".into(), + Self::S3 => "s3".into(), + } + } +} + +impl Fts { + pub fn id(&self) -> Option<&'static str> { + match self { + Self::Internal => None, + Self::ElasticSearch => "elasticsearch".into(), + } + } +} + +impl SpamDb { + pub fn id(&self) -> Option<&'static str> { + match self { + Self::Internal => None, + Self::Redis => "redis".into(), + } + } +} diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 14c989c6..8e675a4f 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap" -version = "0.4.2" +version = "0.5.0" edition = "2021" resolver = "2" diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index 915224db..f589d9b1 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -241,7 +241,6 @@ impl JMAP { stack.push((children, it)); } } - debug_assert_eq!(response.ids.len(), paginate.ids.len(), "{tree:#?}"); response.update_results(paginate.build())?; } else { response = self diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 465f5ef4..7cd3447b 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art" keywords = ["imap", "jmap", "smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only" -version = "0.4.2" +version = "0.5.0" edition = "2021" resolver = "2" diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index 7920e74f..f96ac895 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managesieve" -version = "0.4.2" +version = "0.5.0" edition = "2021" resolver = "2" diff --git a/crates/nlp/Cargo.toml b/crates/nlp/Cargo.toml index d40d75bc..3d70992f 100644 --- a/crates/nlp/Cargo.toml +++ b/crates/nlp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nlp" -version = "0.4.2" +version = "0.5.0" edition = "2021" resolver = "2" diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index fb2d7f72..00d31c2a 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only" -version = "0.4.2" +version = "0.5.0" edition = "2021" resolver = "2" diff --git a/crates/store/src/backend/foundationdb/read.rs b/crates/store/src/backend/foundationdb/read.rs index b6c65b22..65875763 100644 --- a/crates/store/src/backend/foundationdb/read.rs +++ b/crates/store/src/backend/foundationdb/read.rs @@ -35,7 +35,7 @@ use crate::{ key::{DeserializeBigEndian, KeySerializer}, BitmapClass, ValueClass, }, - BitmapKey, Deserialize, IterateParams, Key, ValueKey, U32_LEN, + BitmapKey, Deserialize, IterateParams, Key, ValueKey, U32_LEN, WITH_SUBSPACE, }; use super::{FdbStore, MAX_VALUE_SIZE}; @@ -59,7 +59,7 @@ impl FdbStore { where U: Deserialize, { - let key = key.serialize(true); + let key = key.serialize(WITH_SUBSPACE); let trx = self.db.create_trx()?; match read_chunked_value(&key, &trx, true).await? { @@ -75,7 +75,7 @@ impl FdbStore { ) -> crate::Result> { #[cfg(feature = "fdb-chunked-bm")] { - read_chunked_bitmap(&key.serialize(true), &self.db.create_trx()?, true) + read_chunked_bitmap(&key.serialize(WITH_SUBSPACE), &self.db.create_trx()?, true) .await .map(Into::into) } @@ -83,9 +83,9 @@ impl FdbStore { #[cfg(not(feature = "fdb-chunked-bm"))] { let mut bm = RoaringBitmap::new(); - let begin = key.serialize(true); + let begin = key.serialize(WITH_SUBSPACE); key.block_num = u32::MAX; - let end = key.serialize(true); + let end = key.serialize(WITH_SUBSPACE); let key_len = begin.len(); let trx = self.db.create_trx()?; let mut values = trx.get_ranges( @@ -119,8 +119,8 @@ impl FdbStore { params: IterateParams, mut cb: impl for<'x> FnMut(&'x [u8], &'x [u8]) -> crate::Result + Sync + Send, ) -> crate::Result<()> { - let begin = params.begin.serialize(true); - let end = params.end.serialize(true); + let begin = params.begin.serialize(WITH_SUBSPACE); + let end = params.end.serialize(WITH_SUBSPACE); let trx = self.db.create_trx()?; let mut iter = trx.get_ranges( @@ -156,7 +156,7 @@ impl FdbStore { &self, key: impl Into> + Sync + Send, ) -> crate::Result { - let key = key.into().serialize(true); + let key = key.into().serialize(WITH_SUBSPACE); if let Some(bytes) = self.db.create_trx()?.get(&key, true).await? { Ok(i64::from_le_bytes(bytes[..].try_into().map_err(|_| { crate::Error::InternalError("Invalid counter value.".to_string()) diff --git a/crates/store/src/backend/foundationdb/write.rs b/crates/store/src/backend/foundationdb/write.rs index 1de4d4d7..2ea9a100 100644 --- a/crates/store/src/backend/foundationdb/write.rs +++ b/crates/store/src/backend/foundationdb/write.rs @@ -40,7 +40,7 @@ use crate::{ key::KeySerializer, Batch, BitmapClass, Operation, ValueClass, ValueOp, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, }, - BitmapKey, IndexKey, Key, LogKey, ValueKey, SUBSPACE_BITMAPS, SUBSPACE_VALUES, + BitmapKey, IndexKey, Key, LogKey, ValueKey, SUBSPACE_BITMAPS, SUBSPACE_VALUES, WITH_SUBSPACE, }; use super::{ @@ -112,7 +112,7 @@ impl FdbStore { document_id, class, } - .serialize(true); + .serialize(WITH_SUBSPACE); trx.atomic_op(&key, &by.to_le_bytes()[..], MutationType::Add); } @@ -123,7 +123,7 @@ impl FdbStore { document_id, class, } - .serialize(true); + .serialize(WITH_SUBSPACE); let do_chunk = key[0] == SUBSPACE_VALUES; if let ValueOp::Set(value) = op { @@ -161,7 +161,7 @@ impl FdbStore { class: BitmapClass::DocumentIds, block_num, } - .serialize(true), + .serialize(WITH_SUBSPACE), true, ) .await @@ -192,7 +192,7 @@ impl FdbStore { field: *field, key, } - .serialize(true); + .serialize(WITH_SUBSPACE); if *set { trx.set(&key, &[]); @@ -215,7 +215,7 @@ impl FdbStore { class, block_num: DenseBitmap::block_num(document_id), } - .serialize(true), + .serialize(WITH_SUBSPACE), ) .or_insert_with(DenseBitmap::empty) .set(document_id); @@ -229,7 +229,7 @@ impl FdbStore { class, block_num: 0, } - .serialize(true), + .serialize(WITH_SUBSPACE), ) .or_insert(Vec::new()) .push(BitmapOp::new(document_id, *set)); @@ -245,7 +245,7 @@ impl FdbStore { collection: *collection, change_id: *change_id, } - .serialize(true); + .serialize(WITH_SUBSPACE); trx.set(&key, set); } Operation::AssertValue { @@ -258,7 +258,7 @@ impl FdbStore { document_id, class, } - .serialize(true); + .serialize(WITH_SUBSPACE); let matches = match read_chunked_value(&key, &trx, false).await { Ok(ChunkedValue::Single(bytes)) => assert_value.matches(bytes.as_ref()), @@ -395,7 +395,16 @@ impl FdbStore { let mut iter = trx.get_ranges( RangeOption { begin: KeySelector::first_greater_or_equal(&[SUBSPACE_BITMAPS, 0u8][..]), - end: KeySelector::first_greater_or_equal(&[SUBSPACE_BITMAPS, u8::MAX][..]), + end: KeySelector::first_greater_or_equal( + &[ + SUBSPACE_BITMAPS, + u8::MAX, + u8::MAX, + u8::MAX, + u8::MAX, + u8::MAX, + ][..], + ), mode: options::StreamingMode::WantAll, reverse: false, ..Default::default() @@ -444,8 +453,8 @@ impl FdbStore { } pub(crate) async fn delete_range(&self, from: impl Key, to: impl Key) -> crate::Result<()> { - let from = from.serialize(true); - let to = to.serialize(true); + let from = from.serialize(WITH_SUBSPACE); + let to = to.serialize(WITH_SUBSPACE); let trx = self.db.create_trx()?; trx.clear_range(&from, &to); diff --git a/crates/store/src/backend/mysql/read.rs b/crates/store/src/backend/mysql/read.rs index 5258c96b..0e6e2637 100644 --- a/crates/store/src/backend/mysql/read.rs +++ b/crates/store/src/backend/mysql/read.rs @@ -44,7 +44,7 @@ impl MysqlStore { char::from(key.subspace()) )) .await?; - let key = key.serialize(false); + let key = key.serialize(0); conn.exec_first::, _, _>(&s, (key,)) .await .map_err(Into::into) @@ -61,10 +61,10 @@ impl MysqlStore { &self, mut key: BitmapKey, ) -> crate::Result> { - let begin = key.serialize(false); + let begin = key.serialize(0); key.block_num = u32::MAX; let key_len = begin.len(); - let end = key.serialize(false); + let end = key.serialize(0); let mut conn = self.conn_pool.get_conn().await?; let mut bm = RoaringBitmap::new(); @@ -86,8 +86,8 @@ impl MysqlStore { ) -> crate::Result<()> { let mut conn = self.conn_pool.get_conn().await?; let table = char::from(params.begin.subspace()); - let begin = params.begin.serialize(false); - let end = params.end.serialize(false); + let begin = params.begin.serialize(0); + let end = params.end.serialize(0); let keys = if params.values { "k, v" } else { "k" }; let s = conn @@ -144,7 +144,7 @@ impl MysqlStore { &self, key: impl Into> + Sync + Send, ) -> crate::Result { - let key = key.into().serialize(false); + let key = key.into().serialize(0); let mut conn = self.conn_pool.get_conn().await?; let s = conn.prep("SELECT v FROM c WHERE k = ?").await?; match conn.exec_first::(&s, (key,)).await { diff --git a/crates/store/src/backend/mysql/write.rs b/crates/store/src/backend/mysql/write.rs index 6e0dd52c..141974b1 100644 --- a/crates/store/src/backend/mysql/write.rs +++ b/crates/store/src/backend/mysql/write.rs @@ -105,7 +105,7 @@ impl MysqlStore { document_id, class, } - .serialize(false); + .serialize(0); if *by >= 0 { let s = trx @@ -128,7 +128,7 @@ impl MysqlStore { class, }; let table = char::from(key.subspace()); - let key = key.serialize(false); + let key = key.serialize(0); if let ValueOp::Set(value) = op { let exists = asserted_values.get(&key); @@ -170,7 +170,7 @@ impl MysqlStore { class: BitmapClass::DocumentIds, block_num: document_id, } - .serialize(false); + .serialize(0); if trx.exec_first::(&s, (key,)).await?.is_some() { trx.rollback().await?; return Ok(false); @@ -191,7 +191,7 @@ impl MysqlStore { field: *field, key, } - .serialize(false); + .serialize(0); let s = if *set { trx.prep("INSERT IGNORE INTO i (k) VALUES (?)").await? @@ -207,7 +207,7 @@ impl MysqlStore { class, block_num: document_id, } - .serialize(false); + .serialize(0); let s = if *set { if matches!(class, BitmapClass::DocumentIds) { @@ -230,7 +230,7 @@ impl MysqlStore { collection: *collection, change_id: *change_id, } - .serialize(false); + .serialize(0); let s = trx .prep("INSERT INTO l (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = VALUES(v)") @@ -248,7 +248,7 @@ impl MysqlStore { class, }; let table = char::from(key.subspace()); - let key = key.serialize(false); + let key = key.serialize(0); let s = trx .prep(&format!("SELECT v FROM {} WHERE k = ? FOR UPDATE", table)) @@ -284,7 +284,7 @@ impl MysqlStore { char::from(from.subspace()), )) .await?; - conn.exec_drop(&s, (&from.serialize(false), &to.serialize(false))) + conn.exec_drop(&s, (&from.serialize(0), &to.serialize(0))) .await .map_err(Into::into) } diff --git a/crates/store/src/backend/postgres/read.rs b/crates/store/src/backend/postgres/read.rs index f13e7c1c..1b67db3a 100644 --- a/crates/store/src/backend/postgres/read.rs +++ b/crates/store/src/backend/postgres/read.rs @@ -43,7 +43,7 @@ impl PostgresStore { char::from(key.subspace()) )) .await?; - let key = key.serialize(false); + let key = key.serialize(0); conn.query_opt(&s, &[&key]) .await .map_err(Into::into) @@ -60,10 +60,10 @@ impl PostgresStore { &self, mut key: BitmapKey, ) -> crate::Result> { - let begin = key.serialize(false); + let begin = key.serialize(0); key.block_num = u32::MAX; let key_len = begin.len(); - let end = key.serialize(false); + let end = key.serialize(0); let conn = self.conn_pool.get().await?; let mut bm = RoaringBitmap::new(); @@ -90,8 +90,8 @@ impl PostgresStore { ) -> crate::Result<()> { let conn = self.conn_pool.get().await?; let table = char::from(params.begin.subspace()); - let begin = params.begin.serialize(false); - let end = params.end.serialize(false); + let begin = params.begin.serialize(0); + let end = params.end.serialize(0); let keys = if params.values { "k, v" } else { "k" }; let s = conn @@ -142,7 +142,7 @@ impl PostgresStore { &self, key: impl Into> + Sync + Send, ) -> crate::Result { - let key = key.into().serialize(false); + let key = key.into().serialize(0); let conn = self.conn_pool.get().await?; let s = conn.prepare_cached("SELECT v FROM c WHERE k = $1").await?; match conn.query_opt(&s, &[&key]).await { diff --git a/crates/store/src/backend/postgres/write.rs b/crates/store/src/backend/postgres/write.rs index 6523663a..6373ecfb 100644 --- a/crates/store/src/backend/postgres/write.rs +++ b/crates/store/src/backend/postgres/write.rs @@ -111,7 +111,7 @@ impl PostgresStore { document_id, class, } - .serialize(false); + .serialize(0); if *by >= 0 { let s = trx @@ -136,7 +136,7 @@ impl PostgresStore { class, }; let table = char::from(key.subspace()); - let key = key.serialize(false); + let key = key.serialize(0); if let ValueOp::Set(value) = op { let s = if let Some(exists) = asserted_values.get(&key) { @@ -177,7 +177,7 @@ impl PostgresStore { class: BitmapClass::DocumentIds, block_num: document_id, } - .serialize(false); + .serialize(0); if trx.query_opt(&s, &[&key]).await?.is_some() { return Ok(false); } @@ -197,7 +197,7 @@ impl PostgresStore { field: *field, key, } - .serialize(false); + .serialize(0); let s = if *set { trx.prepare_cached( @@ -216,7 +216,7 @@ impl PostgresStore { class, block_num: document_id, } - .serialize(false); + .serialize(0); let s = if *set { if matches!(class, BitmapClass::DocumentIds) { @@ -242,7 +242,7 @@ impl PostgresStore { collection: *collection, change_id: *change_id, } - .serialize(false); + .serialize(0); let s = trx .prepare_cached(concat!( @@ -263,7 +263,7 @@ impl PostgresStore { class, }; let table = char::from(key.subspace()); - let key = key.serialize(false); + let key = key.serialize(0); let s = trx .prepare_cached(&format!("SELECT v FROM {} WHERE k = $1 FOR UPDATE", table)) @@ -301,7 +301,7 @@ impl PostgresStore { char::from(from.subspace()), )) .await?; - conn.execute(&s, &[&from.serialize(false), &to.serialize(false)]) + conn.execute(&s, &[&from.serialize(0), &to.serialize(0)]) .await .map(|_| ()) .map_err(Into::into) diff --git a/crates/store/src/backend/rocksdb/read.rs b/crates/store/src/backend/rocksdb/read.rs index c0c77d30..287d3e2f 100644 --- a/crates/store/src/backend/rocksdb/read.rs +++ b/crates/store/src/backend/rocksdb/read.rs @@ -26,7 +26,7 @@ use rocksdb::{Direction, IteratorMode}; use crate::{ write::{BitmapClass, ValueClass}, - BitmapKey, Deserialize, IterateParams, Key, ValueKey, + BitmapKey, Deserialize, IterateParams, Key, ValueKey, WITHOUT_BLOCK_NUM, }; use super::{RocksDbStore, CF_BITMAPS, CF_COUNTERS}; @@ -41,7 +41,7 @@ impl RocksDbStore { db.get_pinned_cf( &db.cf_handle(std::str::from_utf8(&[key.subspace()]).unwrap()) .unwrap(), - &key.serialize(false), + &key.serialize(0), ) .map_err(Into::into) .and_then(|value| { @@ -61,21 +61,24 @@ impl RocksDbStore { ) -> crate::Result> { let db = self.db.clone(); self.spawn_worker(move || { - db.get_pinned_cf(&db.cf_handle(CF_BITMAPS).unwrap(), &key.serialize(false)) - .map_err(Into::into) - .and_then(|value| { - if let Some(value) = value { - RoaringBitmap::deserialize(&value).map(|rb| { - if !rb.is_empty() { - Some(rb) - } else { - None - } - }) - } else { - Ok(None) - } - }) + db.get_pinned_cf( + &db.cf_handle(CF_BITMAPS).unwrap(), + &key.serialize(WITHOUT_BLOCK_NUM), + ) + .map_err(Into::into) + .and_then(|value| { + if let Some(value) = value { + RoaringBitmap::deserialize(&value).map(|rb| { + if !rb.is_empty() { + Some(rb) + } else { + None + } + }) + } else { + Ok(None) + } + }) }) .await } @@ -91,8 +94,8 @@ impl RocksDbStore { let cf = db .cf_handle(std::str::from_utf8(&[params.begin.subspace()]).unwrap()) .unwrap(); - let begin = params.begin.serialize(false); - let end = params.end.serialize(false); + let begin = params.begin.serialize(0); + let end = params.end.serialize(0); let it_mode = if params.ascending { IteratorMode::From(&begin, Direction::Forward) } else { @@ -119,7 +122,7 @@ impl RocksDbStore { &self, key: impl Into> + Sync + Send, ) -> crate::Result { - let key = key.into().serialize(false); + let key = key.into().serialize(0); let db = self.db.clone(); self.spawn_worker(move || { db.get_pinned_cf(&db.cf_handle(CF_COUNTERS).unwrap(), &key) diff --git a/crates/store/src/backend/rocksdb/write.rs b/crates/store/src/backend/rocksdb/write.rs index 782963d6..27b94d44 100644 --- a/crates/store/src/backend/rocksdb/write.rs +++ b/crates/store/src/backend/rocksdb/write.rs @@ -42,7 +42,7 @@ use crate::{ write::{ Batch, BitmapClass, Operation, ValueClass, ValueOp, MAX_COMMIT_ATTEMPTS, MAX_COMMIT_TIME, }, - BitmapKey, Deserialize, IndexKey, Key, LogKey, ValueKey, + BitmapKey, Deserialize, IndexKey, Key, LogKey, ValueKey, WITHOUT_BLOCK_NUM, }; impl RocksDbStore { @@ -101,8 +101,8 @@ impl RocksDbStore { .unwrap(); // TODO use delete_range when implemented (see https://github.com/rust-rocksdb/rust-rocksdb/issues/839) - let from = from.serialize(false); - let to = to.serialize(false); + let from = from.serialize(0); + let to = to.serialize(0); let mut delete_keys = Vec::new(); let it_mode = IteratorMode::From(&from, Direction::Forward); @@ -178,7 +178,7 @@ impl<'x> RocksDBTransaction<'x> { document_id, class, } - .serialize(false); + .serialize(0); txn.merge_cf(&self.cf_counters, &key, &by.to_le_bytes()[..])?; } @@ -189,7 +189,7 @@ impl<'x> RocksDBTransaction<'x> { document_id, class, }; - let key = key.serialize(false); + let key = key.serialize(0); if let ValueOp::Set(value) = op { txn.put_cf(&self.cf_values, &key, value)?; @@ -204,8 +204,7 @@ impl<'x> RocksDBTransaction<'x> { class: BitmapClass::DocumentIds, block_num: 0, } - .serialize(false), - //true, + .serialize(WITHOUT_BLOCK_NUM), )? .and_then(|bytes| RoaringBitmap::deserialize(&bytes).ok()) { @@ -227,7 +226,7 @@ impl<'x> RocksDBTransaction<'x> { field: *field, key, } - .serialize(false); + .serialize(0); if *set { txn.put_cf(&self.cf_indexes, &key, [])?; @@ -242,7 +241,7 @@ impl<'x> RocksDBTransaction<'x> { class, block_num: 0, } - .serialize(false); + .serialize(WITHOUT_BLOCK_NUM); let value = if *set { set_bit(document_id) @@ -262,7 +261,7 @@ impl<'x> RocksDBTransaction<'x> { collection: *collection, change_id: *change_id, } - .serialize(false); + .serialize(0); txn.put_cf(&self.cf_logs, &key, set)?; } @@ -276,7 +275,7 @@ impl<'x> RocksDBTransaction<'x> { document_id, class, }; - let key = key.serialize(false); + let key = key.serialize(0); let matches = txn .get_pinned_for_update_cf(&self.cf_values, &key, true)? .map(|value| assert_value.matches(&value)) @@ -320,7 +319,7 @@ impl<'x> RocksDBTransaction<'x> { document_id, class, } - .serialize(false); + .serialize(0); wb.merge_cf(&self.cf_counters, &key, &by.to_le_bytes()[..]); } @@ -331,7 +330,7 @@ impl<'x> RocksDBTransaction<'x> { document_id, class, }; - let key = key.serialize(false); + let key = key.serialize(0); if let ValueOp::Set(value) = op { wb.put_cf(&self.cf_values, &key, value); @@ -347,7 +346,7 @@ impl<'x> RocksDBTransaction<'x> { field: *field, key, } - .serialize(false); + .serialize(0); if *set { wb.put_cf(&self.cf_indexes, &key, []); @@ -362,7 +361,7 @@ impl<'x> RocksDBTransaction<'x> { class, block_num: 0, } - .serialize(false); + .serialize(WITHOUT_BLOCK_NUM); let value = if *set { set_bit(document_id) @@ -382,7 +381,7 @@ impl<'x> RocksDBTransaction<'x> { collection: *collection, change_id: *change_id, } - .serialize(false); + .serialize(0); wb.put_cf(&self.cf_logs, &key, set); } diff --git a/crates/store/src/backend/sqlite/read.rs b/crates/store/src/backend/sqlite/read.rs index 4dc5010d..f1fe3391 100644 --- a/crates/store/src/backend/sqlite/read.rs +++ b/crates/store/src/backend/sqlite/read.rs @@ -42,7 +42,7 @@ impl SqliteStore { "SELECT v FROM {} WHERE k = ?", char::from(key.subspace()) ))?; - let key = key.serialize(false); + let key = key.serialize(0); result .query_row([&key], |row| { U::deserialize(row.get_ref(0)?.as_bytes()?) @@ -58,10 +58,10 @@ impl SqliteStore { &self, mut key: BitmapKey, ) -> crate::Result> { - let begin = key.serialize(false); + let begin = key.serialize(0); key.block_num = u32::MAX; let key_len = begin.len(); - let end = key.serialize(false); + let end = key.serialize(0); let conn = self.conn_pool.get()?; self.spawn_worker(move || { @@ -89,8 +89,8 @@ impl SqliteStore { self.spawn_worker(move || { let table = char::from(params.begin.subspace()); - let begin = params.begin.serialize(false); - let end = params.end.serialize(false); + let begin = params.begin.serialize(0); + let end = params.end.serialize(0); let keys = if params.values { "k, v" } else { "k" }; let mut query = conn.prepare_cached(&match (params.first, params.ascending) { @@ -139,7 +139,7 @@ impl SqliteStore { &self, key: impl Into> + Sync + Send, ) -> crate::Result { - let key = key.into().serialize(false); + let key = key.into().serialize(0); let conn = self.conn_pool.get()?; self.spawn_worker(move || { match conn diff --git a/crates/store/src/backend/sqlite/write.rs b/crates/store/src/backend/sqlite/write.rs index e4acf8de..9578cb0d 100644 --- a/crates/store/src/backend/sqlite/write.rs +++ b/crates/store/src/backend/sqlite/write.rs @@ -66,7 +66,7 @@ impl SqliteStore { document_id, class, } - .serialize(false); + .serialize(0); if *by >= 0 { trx.prepare_cached(concat!( @@ -87,7 +87,7 @@ impl SqliteStore { class, }; let table = char::from(key.subspace()); - let key = key.serialize(false); + let key = key.serialize(0); if let ValueOp::Set(value) = op { trx.prepare_cached(&format!( @@ -108,7 +108,7 @@ impl SqliteStore { field: *field, key, } - .serialize(false); + .serialize(0); if *set { trx.prepare_cached("INSERT OR IGNORE INTO i (k) VALUES (?)")? @@ -125,7 +125,7 @@ impl SqliteStore { class, block_num: document_id, } - .serialize(false); + .serialize(0); if *set { trx.prepare_cached("INSERT OR IGNORE INTO b (k) VALUES (?)")? @@ -145,7 +145,7 @@ impl SqliteStore { collection: *collection, change_id: *change_id, } - .serialize(false); + .serialize(0); trx.prepare_cached("INSERT OR REPLACE INTO l (k, v) VALUES (?, ?)")? .execute([&key, set])?; @@ -161,7 +161,7 @@ impl SqliteStore { class, }; let table = char::from(key.subspace()); - let key = key.serialize(false); + let key = key.serialize(0); let matches = trx .prepare_cached(&format!("SELECT v FROM {} WHERE k = ?", table))? @@ -193,7 +193,7 @@ impl SqliteStore { "DELETE FROM {} WHERE k >= ? AND k < ?", char::from(from.subspace()), ))? - .execute([from.serialize(false), to.serialize(false)])?; + .execute([from.serialize(0), to.serialize(0)])?; Ok(()) }) diff --git a/crates/store/src/dispatch/store.rs b/crates/store/src/dispatch/store.rs index 20dafc15..7ecb8da5 100644 --- a/crates/store/src/dispatch/store.rs +++ b/crates/store/src/dispatch/store.rs @@ -180,7 +180,7 @@ impl Store { block_num: 0, class, } - .serialize(false); + .serialize(0); bitmaps.push((key, class.clone(), document_id, *set)); } _ => {} diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 0cc31dbd..f780b6fb 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -71,8 +71,12 @@ pub trait Serialize { fn serialize(self) -> Vec; } +// Key serialization flags +pub(crate) const WITH_SUBSPACE: u32 = 1; +pub(crate) const WITHOUT_BLOCK_NUM: u32 = 1 << 1; + pub trait Key: Sync + Send { - fn serialize(&self, include_subspace: bool) -> Vec; + fn serialize(&self, flags: u32) -> Vec; fn subspace(&self) -> u8; } diff --git a/crates/store/src/query/filter.rs b/crates/store/src/query/filter.rs index 3e2deae0..53e2e4e9 100644 --- a/crates/store/src/query/filter.rs +++ b/crates/store/src/query/filter.rs @@ -278,7 +278,7 @@ impl Store { collection, field, } - .serialize(false); + .serialize(0); self.iterate( IterateParams::new(begin, end).no_values().ascending(), diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 74200496..dd1834f9 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -26,7 +26,8 @@ use utils::codec::leb128::Leb128_; use crate::{ BitmapKey, IndexKey, IndexKeyPrefix, Key, LogKey, ValueKey, BLOB_HASH_LEN, SUBSPACE_BITMAPS, - SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES, U32_LEN, U64_LEN, + SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_VALUES, U32_LEN, U64_LEN, WITHOUT_BLOCK_NUM, + WITH_SUBSPACE, }; use super::{AnyKey, BitmapClass, BlobOp, DirectoryClass, TagValue, ValueClass}; @@ -168,9 +169,9 @@ impl> ValueKey { } impl Key for IndexKeyPrefix { - fn serialize(&self, include_subspace: bool) -> Vec { + fn serialize(&self, flags: u32) -> Vec { { - if include_subspace { + if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(std::mem::size_of::() + 1) .write(crate::SUBSPACE_INDEXES) } else { @@ -199,9 +200,9 @@ impl Key for LogKey { SUBSPACE_LOGS } - fn serialize(&self, include_subspace: bool) -> Vec { + fn serialize(&self, flags: u32) -> Vec { { - if include_subspace { + if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(std::mem::size_of::() + 1).write(crate::SUBSPACE_LOGS) } else { KeySerializer::new(std::mem::size_of::()) @@ -219,8 +220,8 @@ impl + Sync + Send> Key for ValueKey { SUBSPACE_VALUES } - fn serialize(&self, include_subspace: bool) -> Vec { - let serializer = if include_subspace { + fn serialize(&self, flags: u32) -> Vec { + let serializer = if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(self.class.as_ref().serialized_size() + 2).write(self.subspace()) } else { KeySerializer::new(self.class.as_ref().serialized_size() + 1) @@ -305,10 +306,10 @@ impl + Sync + Send> Key for IndexKey { SUBSPACE_INDEXES } - fn serialize(&self, include_subspace: bool) -> Vec { + fn serialize(&self, flags: u32) -> Vec { let key = self.key.as_ref(); { - if include_subspace { + if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(std::mem::size_of::>() + key.len() + 1) .write(crate::SUBSPACE_INDEXES) } else { @@ -329,7 +330,7 @@ impl + Sync + Send> Key for BitmapKey { SUBSPACE_BITMAPS } - fn serialize(&self, include_subspace: bool) -> Vec { + fn serialize(&self, flags: u32) -> Vec { const BM_DOCUMENT_IDS: u8 = 0; const BM_TAG: u8 = 1 << 6; const BM_TEXT: u8 = 1 << 7; @@ -338,8 +339,8 @@ impl + Sync + Send> Key for BitmapKey { const TAG_TEXT: u8 = 1 << 0; const TAG_STATIC: u8 = 1 << 1; - match self.class.as_ref() { - BitmapClass::DocumentIds => if include_subspace { + let serializer = match self.class.as_ref() { + BitmapClass::DocumentIds => if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(U32_LEN + 3).write(SUBSPACE_BITMAPS) } else { KeySerializer::new(U32_LEN + 2) @@ -348,7 +349,7 @@ impl + Sync + Send> Key for BitmapKey { .write(self.collection) .write(BM_DOCUMENT_IDS), BitmapClass::Tag { field, value } => match value { - TagValue::Id(id) => if include_subspace { + TagValue::Id(id) => if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new((U32_LEN * 2) + 4).write(SUBSPACE_BITMAPS) } else { KeySerializer::new((U32_LEN * 2) + 3) @@ -358,7 +359,7 @@ impl + Sync + Send> Key for BitmapKey { .write(BM_TAG | TAG_ID) .write(*field) .write_leb128(*id), - TagValue::Text(text) => if include_subspace { + TagValue::Text(text) => if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(U32_LEN + 4 + text.len()).write(SUBSPACE_BITMAPS) } else { KeySerializer::new(U32_LEN + 3 + text.len()) @@ -368,7 +369,7 @@ impl + Sync + Send> Key for BitmapKey { .write(BM_TAG | TAG_TEXT) .write(*field) .write(text.as_slice()), - TagValue::Static(id) => if include_subspace { + TagValue::Static(id) => if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(U32_LEN + 5).write(SUBSPACE_BITMAPS) } else { KeySerializer::new(U32_LEN + 4) @@ -379,7 +380,7 @@ impl + Sync + Send> Key for BitmapKey { .write(*field) .write(*id), }, - BitmapClass::Text { field, token } => if include_subspace { + BitmapClass::Text { field, token } => if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(U32_LEN + 16 + 3 + 1).write(SUBSPACE_BITMAPS) } else { KeySerializer::new(U32_LEN + 16 + 3) @@ -389,16 +390,20 @@ impl + Sync + Send> Key for BitmapKey { .write(BM_TEXT | token.len) .write(*field) .write(token.hash.as_slice()), + }; + + if (flags & WITHOUT_BLOCK_NUM) != 0 { + serializer.finalize() + } else { + serializer.write(self.block_num).finalize() } - .write(self.block_num) - .finalize() } } impl + Sync + Send> Key for AnyKey { - fn serialize(&self, include_subspace: bool) -> Vec { + fn serialize(&self, flags: u32) -> Vec { let key = self.key.as_ref(); - if include_subspace { + if (flags & WITH_SUBSPACE) != 0 { KeySerializer::new(key.len() + 1).write(self.subspace) } else { KeySerializer::new(key.len()) diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 742ce018..67709495 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "utils" -version = "0.4.2" +version = "0.5.0" edition = "2021" resolver = "2" diff --git a/resources/config.zip b/resources/config.zip index 878944dcc0cec168f0ef9ac0c40061b9bf61fba1..2b1276b08b6541164e8b612b8dbec941f8fb15ab 100644 GIT binary patch delta 33919 zcma&O1yo#Hvn`CfOYi`}A-KD{ySuwPp>cP2x8Uv&+}+*XgG+$NIp4iEIrp76{=dhd zNB3H*_h8SeRcqF)>K+b-IcR~Qmy-kqg8};M5tJFH`uB_fdV&PP1v0d?F)=r#RaSxq z0{s=|V~i5-6HAYl46p+E1Pk|jJ{|;(3g>&Ihbo5mt<31h=iPvTfUy3U=SRZ;;JFwY zlWQ3nJG;RG0fXIrtN=s%Seh8kYum#I*Y64lBToet%|U?$l_Y^6E{F>0mdG$Tew*UN zm*e@C8C1QyUw*jh$)h@t1Fl@NYKthG=T=w}4fS#fc4?#pBAVAp zp1zZ>&V1THkLs^JK{Q3QiVL8is0XWOSvv_AzHgY)Ryvy|O|$w|jn~g~b94lD91+W5 zJW^Hjf;+e$0LuxZ4qzxJqFu|22r9HE$w7Qu4CFea!JxD=J!a0Vl$W$#t{LE>eL2K2 z9P^@U=x$;ciBxq52mA%*U-twBM9QbAI!lAZ9sTjn;c{|C)@J@i3(@#=q~jrea%k-Xkor}9f7heEESzA>@adubZ6^@zcvab@-j>@BJDq=^`uX3?U~c1N>|mpBMdM^^ZT0tF0CW5| zpaDRMG1C@3bZ9}BlZpXC-}7MC2WTJ>oaiv&SX>L8`7jF7*W)DxVPYs{$JI#X76wdn_p#a54l+OGk)>t6VUo*0nm$GM?)u^($9K3_3Tv2V;2k6)&BR zx^8}G464xN{xVdXGTeRi1THm_r_3jpSFqF8pQS_PQ;+!Dqp=@>{|8N6pfRaWQPC(!u}0sGQTAVI^ZLY_^A9Fc0d&g^0KRRNIsjdRC#ETU@LM#q}F2SIfUon z)@P~c$2(e@wo#eO$up%_o304fEBsRdYWNnLPZw=%k6s-cRNi?Q+S)o=qRAt<#HUHI z^(J{m4WJ4OA&b=>0&8eEGM`RCO5MH!%+&Rn!afTF3vW-oMDE-kiD^Q6m~dxidHq5Q z$K^tRL4|{1?*UIp9N@97NFT_P`|N<{wr6n`lDt2HX802-#$<1LJh3lrv9m(dn}%QF z`W2y3^f<;(x5aC1kQRI>r)XI%N1coj_}m`n_DnaIqq^Xgk-*IQ5@KYGzs!IDFdb_< z$$^Ax8w4yMqhI}t4j1Ojc$>0u#}{VAv)A2aZ(atrKo$D05BRMijWE((q2dJWICebg zw41B*<#{Nja-MN`MTgtAv=^dA{t~I7+?$#mfRKF1?NhIHpb;s}IYsO6HfbB1x}W;b zM4Gl))u2hsWbkc7uOk}VpGdw!0M_P$CI#rv+_=1kIf#_=DnhATpyrq^hNlL15YQSx zIB5zuKD%_0N}#sZ(8;m547cs^)I7;HK9_gD$T!FI2PQPKB=GP)?O16Snk$qUI3*R; zbfF}8!yfZMg9S042C8ha_9R}T<=LgMb1FEhW^HbfMX911?F&K)Y+# zv`*4ddEQTF!RWz`nl%Z$_1vY8njS_U(3l6Ei8z-cRnnjSWa_q1MMdFN|QiY%jHgl{&G zn3wunTj@i^!QP~l;!x8$P zTzmBGR65u#CAM>T8%~T}vRClDi(4#K%Qmmi@pcWZy!y*RD|Wy>ZPo*Q(AL`ZPMP z$RbLMQ~MSJU|z7YTlJ7JgV)+o1}g85o+MXI)6xOO%lHj6@ciR{107ImF@^~R_e+@s z{sU>ZC^U-#IZ#`3Rf)?A*&x)bNJ*Aip2#|t`J5+FEZDTiPpc%#{e1aPptVsYc0|z5 z$=*Fq2;>Q0Q9t0vCce^ij{?Z54ZSeLgyqU}2Evo>1eFo^QW_YB}Cq$y&Z2{ znUMW$0ile6SmX~2aC}%G8w9az{fd&&VH5yd6kNPvuJPX%cs}*A%h=o)&%!86e%f_# zdAGyv%MOtJrv-YILm=-zEbt2)=r8>Lhll*OfVHvp2i^Gp^!A2$&5sa`8Fcv(qKU-_ z%*X@3G^fnj6y}>zqnri^VUP~GXvqEq#*Zm2egEd-AxBXm!ZeN%d-HU+d-*L>NIno4 zke4%P`K>1)4FazeiorVwZ1Q-C(yU}F2ZjzrFF-Ye*ocH($~RR4#S2CS+~<3c`nIHq zdaa}4mjYg$VjzBhQf4(YD))NzRB$+nNgk=FXpJ}IU@~9iP9b+1ji2T8i2m$TEdH2O zdCdE}%oc5}1KjvgS@E}zN2awIRyEuUfSO537R+-80`&b53AH08aNJr3kPxi>ZI0M| zDpG6;4UYTyOBEDjW_iLXFbV@NWN89?R1pC_%z}*<@&o}Khz7z^XS&lhr&Zogc!2Z8 z%lU?$_m*GP)RHaz@=>^hlrBYMwcFxaCMpCTKZ_@)Wri*jE6z>5Hg#sRe>K4iAZe}A zXwsoIXA!1U+Fi)KjWck2PlhKc5Y@&Mm@!~eE3WvQ@hs^rUSD)k zMIu*<`4Wpd4AK4lEP!7%1g`T*YIlICl6Cj2;o;7qi@cNPboCUnZ)sQ2d$Yac^zH{- z6&fDb0vYQy|{z7*M<-b=`@9DZM;4om= zKj?qmS5PKkoO7*s)hiJ*5CTvjpn6CkAk5#NNd9RKg8COX*l_q)ZqT4?ZM#B;^md`E zpQ`Mot_C8#>YT<@->6aOtW-Fwqzsk1NANi`a~UBp_w6AQwMTQhKti)KW-T+j?eN#y zv|0NOA^Zvk9jJy_8LS~;5w@-{1e1cpSJJ=(6>@PM&=rEcX|_4Q0=6@sf&L>oIPDHQ zeyEZfaFBOFH0YN^bVKAs_jZBsnK1?$g|a1|Jq`5`q+Us%!5k`A+3?U9uDj>{BeK?I z&DI)i^F;PuJRt%Tq_zhe{Y2P_0O7BF-{cLc(2tnZkS4n%`DLv9T6%J%8F(I~3WRO6 z?_JykN@*uUHhq!+UAFiyJoGBC?8~^^_nX-&R3|f9={Wyh`X{|>5 zYUy9!&NK!jh(J4rS{e~hPV41&=9_7ncTt6UvaE3Y@3Fq8wMhFwVHFB%;gu<#E#q$jeEEtc`9ggirouR`^SjJi|s z4~P-T4=hXpL`P{m-a5hsW9~9&+R1&*!hqF~kcdcYGx)3@7tqd~*T&o8gJ;5F+_gvV z*7pxzHrD+m4W|&>-gusAGHvmjKTlY1J7@GY&ZB=;FgU6Tzl4E=IrKp_xWwjxV zY<%UP(7l6?N?#uGTbk56H~a0HYbiL)sGNBzeb83aZHiIgkZ7)ukcB#FP%zTVX=uPU7JGo zDacRY6ID#cw>68-c)@?-WM>zg!bONL>kyl)n0A=WGE_{(;V@!JJP}8bo~VPn<>%oA zVoyl!%7=20d}&%w@1T4KickNH{g14?tbE}U8=Xdxw*8107*pV!4o6h$^c7DMGBpyrwJxFjDC=}{Ysyo=k1|}JIXh@NSz18?;d zD_>Ncx*2Glvb8{Th==oURs>?%jZ9Db5FutJ&5QWVIgQ93V87q7q%y2|d{Ux2m4uan1dMhkdJ-8YtY}i9 zGgf9em6B_SZF{*SlOB)8Zr5Ga7%MD9*dhaNPDfu)EF0PZkM}P{V3U&sm4_KDJy_>z z)K!-c1k$I^37n_v0z_Z!+V9?q{1fM6KS%DW=H5tlOv*;VJRh&aM^nxhh#Nkj@3pJ< zfBjK8Um|943zX4t43J7^)y%CtK@%<`moUG#Jt)^DLED?&wIBQvv|*uCDpL66`h9U9 z+GZiAX>70DoUXHuwU9oeE-&OzIbo^Ty|I){=NDP8?KBR5{%#409%Nusx4Rk4U@rhdSr%R&qg7F1yO3);6Z!a(6Ok!KM zuiH0O;hvwnY`bry;hC?-mK!K7X!9MCI!Ls7mJCr1hbgfn-?G9d-g=#QJX^DAY!EX? z2gubZK(VSs$h=sMNcI{;0HTP~?*hB|n3(q6{mt$Vp6Bxtz4=Nro^oL%bu^{Urwfxf zVo@4P$|InO^b`G#G^&G%W!9ZW;>QB@GkluI=j&efM^GmE7_&3VXi=9f8u81f%2JM| z1+|#E%SvTPnm^K)i;tUV!;8}=5$mGE+k!8L>V+W!22j3k*N+x_0{pV;#^UAmMBtDE zw-kTyrA;lJL8^I|Ln6s7Jtr@uqwv=5Af1Wi_^M80-P^0o#46Pv5orBBW+-)2Vx&2lOihXER=d^riFh@xzLKgMcp5Rw?k% z=_&g1)r8z@n4gM>Fu+LP1uC0P@w7z3lt9$}3V&b(2EHl%B~C{WAfjX2u?o z;u;A=p$i!^f@rbj<(T+%1S`*~BNoZG*^SJH*q!P@L+11q;TEANk%5R$9-EaCK^^{W!@HTVz$G2n7h-q8rKC#*~}Mha|Lcbo7c+*vtFDSS6KAl@9TC*G5<+--pEvd|0GCtY7|DmIMyFNt$x_mYVpB;7~uZFf8wVoKLPOm z-H97iv~5>e(7qj35tN{r#SX=;H=j%TT3JHEsnz6-euhw`#ylR`5r~Bm-_H+vf1x2+ zuEC{w!>MXR;vdfPimY|a0gWgd{;ctMJp z;MdnrTpfyReBP*?t_G}HZ3)*hM^A634h?E{ECv4fh=?65QAXD$#XaCxSu)g4Mo|t5 z9)u^~d<)xXN5a#LtJDVgCS*3*kVjB4IMfVy*PWFK;E9pZV};tiw33pL_d}B?*(dEG zuA1wgaFpy4Z&R1&sMiP+Y95H7k6WQZ*Lljx6l?Zp+WVwVULqZQbSvkRehul?!LWS( ze#u;>WlX?C;Ja8Q@?=`2xS+f8xdaW?C6L;SCk-Nf;>FH`4hADh#EfXoEk2@An{1LSm=+;L_95oSW2G8o+41wO-krlb8u^hQ&r~>1 zoD4z|gbg=ykaNN>-r+%==v#_=*SgbP7)5QSjwtX4dichDdqeZ=3~#BIA_POgEWoWg zE%G~mx2>EYsyvL;cSHl0OR1ACpo{qnow`ZLH17SVK_ksW$nX`fIM@2Fq;@Dq!gcP3 zRTM+F4KbP+c?$4H0jHI*eAyHKkh>96YTbp~b&Te;25_S7HV=)y~`&^af?`UqU$ zyWsQ){+HTr{6SP&l8*FM1bNBuj|-)i{?-W%5|m5kKzeEi-<> z{!X6Tb(B<>1M{h?KLUUf_&up-zv^8W^WQK3=ukkEH74BW%EaoLzq; zLGj!47=V18e?O=uv?Y7#&>*&6euc$Uug<`qN_4|6lR|~n&cjj&*M2R7yq1qsG!A)R zw-Uy!WRzj2HK&Ill?pmQSbMA%^XC9npk8`P9X!k{1=oEh*o^<_ukScw{bHwh9LKlzx5NmQ zCyfvSAq}*jOrE$*h!X9(wX{wB;Z9ug~1%`g@Rn!TX@r)u8U?Q27#sgJ&^qF2J)$HoEZ`vmh7LS7wG zReLWv`6B59i1u8!@mXy+uoS)4M%#-_yg15Em3@aspMLGwjxn2dAHeP#ydg~j>7xT=@!~DvucV4Y@r()8 za3~#}9Mo^0!wv+;`+_$=FA)v<(6a4vUwB<@2EJr#sUJ@64_P@!p_KWJ)S!ya0(ltO zAy{Zj^$Is<^vAPA)5mJ5@iexKm=Szl}ZH4(kh~t#oeje%I)oaOuGZ(=-|iEqzOrcv03H2kU7T zE%)(m=`kWU!jr3*uSpw+FA!=X3yFEVKp>6kX$ zPYtX~JYmOB%O~5(fwgD#i(ap=-TF1nc_-;y<5fMXmb9EyP0aoI+YfQ-9a85Ic_7fO zJ+2kVw&?oRbdxiswB{rehB2quY_@Y>Kna&x-RLXE??WN@!#Q|UM1xRGp@4wezW@RK zxo7B+LB0Sg|J^Y<)U9n-*-$=2tZufhm4SKj@Y6=u)q9{p6;ypg&gx79|6bo15>iqN zah+J3w2p%xmoam&6tT6AU0K_RNo*NdJQ=RDoeu*%M%sZVeZ4M>`_4@SFTku!^k$&j|5E^u@2lB-P`z+v7yTp9%6G5J50KnrV#Hy zkDac=Hfz54jU2vXwDI_J)ESDKaN+0#+Zk?Aip{S0n2ahqP+j|A)eW;9>MJ*A*WK-> zv*${`m(K})07*|2wPV)!bofsq;B#F4tGBU6t1WD4uPuCAXV8)$;DZ$`u*l+^@K3y1 zb=+GS$#ofxstBJ3N4|vYOB5{3Do|KeG1E2UC{T53*nR#vU-6P z`s|27MJDPV=rCx*Zcu*v5jNsJ6us2P2yJCQR=_^%W%oYHc~X_aiBmv0gE=_ij5+p zJ&ia68zEm*hU9dK#ZWFE6@PpJfyfi9P_Z1~!emnqk_#pQW5k}dj+rJsR4ti+`zBO1 z`387$gdI2tjpld#EPUj$O>|6YCjCf67%H$FRfg3p1TF8{R^%dO4(V{^=FO?O{bXdI zT=~m5B_+$&@HtcHUJBOPQQ4-hQHm;DntQ&JI8<3Vq5;=tF34Sa4F@eSNj|B8-F%nt z0QQ#ENyv*=XSDRhT(!)1i=I#(P@1}BN(bnGN}QS8M^BFAk0TYdKj6*~`O4Y8t|Y;y z4SGuS>>0jWQe<%Qejt6-|LfZ2i8sx3d4X9fK^>b{+i!=49aVE$JuGHG>Cn4WqnJS` z56Mqs3`w@Tc-oBp{#!i+6NRJ)0+g`el{gC+(sWqxgXIdE$rswMX@Lo)PKf=UZFhhW z0%!U(y=l_+A6J>|#iDYlimUP#ywwd6P;?-+S8Ykch> zik!_dx;^Nzb0Tt6irlklD%9_~J67P-(G|S+CO=SCv7C(F8y^U7OGgiAC~OuU^j$Ly z@IY`5<|#zOy0!8Q4_$I1P$6Ur@t;L#O zO#1!1bAPGMVN=dX{n{V*I<6W!u79%^#-Xmx`hs5>+_ijjMqBFc2v;JfIn>Py;Et@yJ}(=M#8JYNN7H%4zS@U zHcJ3oE#?g|K@tOtml;-}9s=aI_Qw;%+e#$Yt)(_jH-9zsiLX?LqM&e4(2NSUHGOmA z`A&9aI{OS(TniY-?4p4HIIQfcCPVo?joJ8H#HrHb@IloKlE^#ni@YUrZA13Fa5uv4 zI6h5gsp)s9mh9YKF237Cl*9DY0H6l(0UORI60t8-PF;dD)YJTN~d05mhwVy`Rc3);0!zV?OXP?WBX~A zYvKDz4lESsQT~(75$l)dQodI%HXl6d0o6AQ#Fx6A;yNg|W&H`nIp!TlFEy^AlY%+^ zmgb`2=+BQ0G^ODDyuP5*Vu=3Z(rg0o!uoQmcd!-@u zv6vT$0ON`FlPR%U(B2|x!|4ru$RX80#WbKC%JvWU zllw3GckFZ2E!%t{sO|K@kc}UspeX{=b97R7^zJwqgk|?G^GNHOkkg~oqeLXTWrqsp zf`QudyvDIJbzYd=RcUFJ)O&oU*%Zuk|wCL=q%GxwJ~Wi^^@m3)ZnxOV`fL}S_gvcC`2Dm!e(`)F}F71I*9%H zUc$W}JOC^5XMUq<&FqkAr-(hz3@N*-8`r^#5K^wdBK}cpZi|5!lsR44@H=C}4tJj3 z&)6dTzQG*6!HS%8iZb7zs3hVU2A9VM$iu*oGDDWK-CkTMjX6h=4BE2}n}u!2WK`Q4 zq6aAD&uMO#;-{rze^+{W05=uBq%n#enBbAq6(IF|RIW=});4&4P;m_}AG&Eu%LW() znsZKv3IxGsw%cV0Z5T)TfqAg4)oW=0QCFPH1-AJ0=qHXEH#T4;Nrxk0Oj^{Qw%#Z! zOpHFL73qZQX$2%eB`lm6I9oX9o>2i-B&(gK#$EcH^R14)j=0u*O*zcjCznRY4f2^kJ0sq4*( zj^uo^0-mmFi^ntC%Ywk;AX($BM1U|ZEYTc`2ms+3@pty{^`jg&CV_1@@R3Dhex#Rw zq?Z3xiAx@W1%XR$!TrcE_cZ@iEi#Vg5C3Q~47hwkue0Y5M3GQIJP}9cf`)wol~|G? zSz#f8wupUm&8LDL<=HS#c{<=m#w5&d(jF%#L>zsyVIPn5GLigEp>LzgF#w+a^A9<4K$1D|N1-+ui@g_QYU@679cWT`YXLY2P&br1^%XLe6#*9vIQn00Rn%5ZZgj25E{Egksbe|< zF~m7L4MO`yCEfD|-?i*q1+E45O1L)9=i?K$+&ZZ1RUP2US-u?BcaAe5L%n**0vL%o z)_N;obrGiMK`68Ylf@v2sheEBiR(^6w8Mz(qF+3rGz?1ArE}1n0?ChH&dWSQG^1j% zlj~UBfJ;o0KNe$({36!+whe<_mqm!(PXPBs2h(HLQ!Lv}lr1!SI6bytHBCzne1OyO zP;T+d(F3(`KHE88t(-CW#-LIi4^Si2W$3bja-Evk$_3dawGDuKH`u9vr&LrFs9U`R zjeqp5G2zY9=fITxa&kk6J@#lN){iilAEV$*UAN%BuI?WwRCDrbkySwXiNU-DzeYsz z`0jvwA2-AHw<**$gjGQ>&1%U%#7mct1oyv8@jsSma>obJ0KxJ2u-KIyDHz4a5{|DQ zU37owMjwI@TJj4a2s|M9-xJ|^4NKc}F|^k#UHVh8h7M`4ym!84Ey;lrNHign6p1iT8hHi8SQk(d_-?Q%wU+h)5L|xyo6=Jsu+vwCswiFzLtZCeBdAB${KT+^SV4$)~d>25utR7l|vx#nVq*wze=%4Om)nHh%)dz#9h|TG*1_f)brFssU5l2$iKBQCoR_w%EsfJ=cGM z7N5N@*~}702I2E)g*~+f49+mCvP81mlmrpI3x4WD;vaUtpkiSgR8WEKcVwIOF3HYm zHK~|98BbrJ_BuEr?1%(8f}q3Yg%DYV{_Mm9z;T%5Ba$n&hf>*(n68dzd1=X5!Wzlt zG5XFJ(DCHs^>~A)k4HQijMUn*rEp~3&u)OkSm5q1XWkm;gk@OJ!F>anNE^H$i+Xwc>n!KmDJcGe zjDRH-#!x;!Ym*DEEC7+4+PlmZ*tWwn7H zlq*M+ozrS5+9Bs!56L9LlnMuavSe{gM{t3ZM?aE9-q})>05jf{qz7N(^D9EUHO%7S zTJ6@+g9P<6tdq$PU=$v-dLYn5W^iYoZySgnFaguJi{d(aoSBY8*b_e&XD1yU(bo^h zMzPo^_192^zH=hSAEpQ+UE2V+$NZ#Yh$QirsDmp5HmK>30ET&yqrwk!<_ioE70(;X zeN{*UvAVOM1$@5N^LVmoTcBZHb>vxMJCtM0yva1Nx2(PRykLJ*6=7>qIy`H&_|XD} z(3g74kJhy>MKd5UL&`W5)o@DLSp02U4=JHV0dD+G!qS$jnUt_LV|ZM;?BrWc*$6(V z-EmRv790m&e`8`xF=Zl@z3X-@JBya)^m0#wk6JNx7~uS(e33?im}Sq&v2)HBxOgYO zcFpM#pLnbJMHj+A}DjzIfXL575?PU64HCF1g2YzwXdFJF4B@}oJfc-wF;x`bY>+REVlR5B09EMlfXA4IE zxV4_)%N-gYU#a4k00NL6<*l!a_p7Zul~%s)kN`~}LXNhL0R2(MEF*Ln0(4d`&3shu z)$B$(yv5>7qkbgB4fK1fROcEO>TkNMKxA$8!=z{QaY<=`Ooq}3s1!kAUE{vr=}bq~ zx^=}S6ZKdH@M2$NHBhY%L{Z@!%Del{*xy`yTX8y|jY9NBLG2tcP=DTtclle`*JbCI zw=na=Hq0 zSh$)f9FxzFgoEM`tD|kbB@9q0aQ2twc>y|=3%tWr&ygNgrs?JSB!Xk7O(U{T?+-Q* z2pi&Y(9FZ}Vs*=@$NRe98o^TGmQZFt+7_Q0 z6Y?>1yRd<0q8hT1}$dhi6n~1K;z&i|9{CGw@@mhwbX2SD3>W zq>3bzLGDAMA)F{^+SfRF$9MgRJ^?JM>AZc(O1@O6CwmAu%O(w%YkbEKd-#l&1xd9= zRQ(!$4`uo8$KAsrRMQDeg^ICMyI!TE2-rhIORv#j)X^NYS^#LnN?vBdO8@%8?_ZNz zvUJ-7jPomeA#I*=&}jTK)iUe;o{qA~VAoXj)$W&O^99-WS9Zry_oZZ@)=?)TdCJFTh zfFKgP2uo0hY54_dJaiqxO+A0KX*0l!5yYl#ISSA+WMPzwU9%3km(~r>XuY?RwJXpJ zvm;>hgZxZI*t$%5k+PjkybVC%i3~7nBh_KPTUGdRa+m8@Id`*l5tB@eRVv4i-aod$ zpJ$@uCw2-#>C#}pT?Ju)M0GE9eXS2sr!Y#?6ktT)?{RH65y+1*#JULn`GEH~P5Fy| zFcgm)419dYqz?xO=#S#r?}oNyP6QCxWG_k(6v+Q#BFTxAAbfyKV&P$%HfdP3Pr=c) zG!ASRk8J$w#I*)5mFYZ>(|A$Z={p@Hjoi2o@g<1<9T)g`4uMw{=lK^1xt|bJhF^79 z>K29HU66hyq7djQCda@O1iQG#;sl0D3#3i1imq%`EYMxMW^ziUu2~ypk!@nE;vuLvEDuS1;z^b>uN`xGe17pyxono?qsgzJV9o-W8kdQzjyU*B zrc$S7abA<``eJ$l$h-hW%knf!0l*+i}NIO8Ij*Lq>=kU!c z_-kC5TqS9ap_5v;p^Y0IpJ=-QdtwWZrrPtZLv3{ts=p}>rY_iX2cFqa`&N>vS$Mw7 zg8`L{4Y(=5jY~=0bg*@-P9st30Hk3p$=tMvB-Snm?Hb3Smp(Tynm{>Bh11;>!beDc zTyfUrtb93cn*2rzncaa)22K#xj0y%c4?Bv)U5>b2m2~cIpD(6Ib}{1zW~6oVQ)$aa z+zSp)jpg&g*gy!flh32}7d*Z2ZZ+Wt)F&-Im=-y}`LmUTwX9^f!)&5SLN)ePiV?hQ zj4{gB4aQpMZg>`@cHwAnMKnOeqq!Qtkd2_yNp9l7!c>!O$y=I*w1tY$(ttZh%|Q@P zT4cmx5^EExz@4mB@?TzTniy97hyC z+qHvdPkg#BNrI^3t;7(Tc@E!xYO%MV$BrxmG}>($1Q+}?2u7k557^>`uKF^!by>ih zmLZ*6T0JxQlZ3I51DYyurro$iqwZE27hL62kEYn6>3GW2-WCHGq5>-^!o@CN3*K6W zGqt0~2|lPb%wQ{I7LNbw47oLoyeudd1ANT?ZfyjjwO=qv?GATTq~SA{!saUJaxK<5 zAoL!5Q!|zQo~gCgpDf~)e?oaxlp1rVRQw%Iq9Av4m`3F+B8Ys{APdrWj9?f1@Jfi1NmfxNzydTr|h^hYW+&^0@*4Vd%hz&Q2} zx)b4cFhxasScid^GlC5%1tD7^O+6b@Y87!!x=u`*TUR^Bx0pmE=NT{h)%Li)PHT8^ zXc=VvPzPRHwh5hLS`i%&e+6o7?ZW02MIK&kT5e|Yr)wgpE*KS^QipH~8(P0G0H^Uc zMQf0klCG2e$hceBUDYpj4tj#%PAr+l=u<2W9jJycqX#JBUL)G~P}n5Z~9e?VmbkUi@abU6x}*{Ct!?cgx3tu}IsMTa!#$AmHM= zu^{Bhl~X@?+=JP?x7yQZ;>_5n0MznI1I^ILmdc!p$43ySBo9nk@)s;tL{(Xc^=QWp z&850$GA1wrC&%Hz7M+B(P%6FyqRF8{YzHHqT#3yFl5L6bs62QJsF-F8vby7{IqI#X z6E4@u^{l%Bg!JikV9-)mQcg?3q#7xvU29RU%&>z+cQx%;zYQKE*7d&3mMy z6X&fZm`E;Jy*sLHQCNdWa!Spb{t#27pn0kqkqH2)QX>Fpr3+h4W-Xcz2$GsHk1BC zvjunlSHZSHRr9ajLGQ^bJ?)&(wqeV#J7G&0@){J&JK?&sK23R~h`uE&BMFl4q#G;m zor2;?M(Qr@HjE%b?cX+AerEI@n(?U(J>mvRQ4+t3$+@E`1Zs>T759F!c#`j#ANGs$ z36sHR!IlB&6}?k$(=K78!HXbfgq_ui_OiV=5Pcs@osQ1)>XEc5q+PQ1HXGUZ*y$aO z#(Rc}uZ`5nQ_Y-RwAJ{5Tf-^zBc>V+szbP#`gRf7Pq(I-LJOg8qY&Vn2~&^nb39H( zFw@p~E<(LG(LEiGQPQd-zn48z>~T@#3VvtK*P;t3BwQhT{= zj_mXnhP3LG?s?gDdGQQ*Ti7aaH7!h5z@5iZllgJ!hRVkgShpD!m6e~m4f6@QNJZ4S z0Y8#Ww{)2?&@)04p3^ZFreqK^Gc#9JQ{ubKpk4l5G zNB@>jm^p{$OC`DLfDyW49VS%IJ|23|_$#@OmZ3{%-a%nVQjsWo2IC+Tg6aS*TjhTsey23~ZT2r)GI8OhvvKWGdMZQ61UlUL-eRr3{PEle& zSYL+SJ(>L19rmjvsCjM^mYCz5;~i6aPHf$Mm_=yQ0kLZKU7)VRpxuiv6_=~y%sA{e znT1)`t~Ffp*py#67zHBSW^X7LeZ;?y-e6D`G!n=rH7OAczy|vTPAm1?)gIWbI?58D zEJ1tU8d2VL9J~))ZwSU<;mN+=R6Q=LQSyr;xz@7uxlk)90m4bn;4z}gk<`YMD$<^)+ea}bCHj369WPOJb6m~u}w z=B(%(_)eL3T%A-eN2ooudi4c~li}ys)J*C!?=A!J&7CXhl}MxJsb?(&xU-6qNrm8S z9t!k&henf7*2pyI8V-uzzCXGKioGnTP~+Wv>CR}Uv+|K=j&4zued;rc#uBm8j8w5m z=t>3zFfYlkPRgwWM(`dsxhgP700qJ|Y{55+ zD=|Ks$#pS-&jlfB=i)+3caTU5hE1jAUsnV1H zvQc)DYJ8+*=Em4wJc@l!OErg^HPg|{f@a4pPF!_Iv< zpYUAII~jcp7%KbH-_`}bx6}4s{voiQxkRY$T&lmVeQ4b(AH8M&TVPF2_69-ttB+99 zs%exsEZ$3VO=!^&lXG_V z@zu=fCleXjD%At7X*yZIbt`+g<&efNe!UXKxUWhgtUla;9}O}|M`C=o!)nzH7l}T2 zzF(NAH;IA=56s%vjm%N@EnU_N$_kcfvPX1L&#t;k6>=MD`7am9hZd$fxl2(wso`)m^t>kv*DMAEoSS4*H86*u4q)lea9JyAxfq1zf z;m*CKyf`xf@LjY*cu?xfMkUnHl-3~kk(1b8S=MrI$WGP1P_N~l9GCZrW{aBF>pY5# z6iOwqx zeNv+kkv814g`e~AS1y~}?fvmPDu4Em{e7bU@&D*L~8)};rr+0V$_K#HBW z9cFp{%mft^izkWgU0WnI-w{RX_&;@B1yq&I)0az1cS}h(NJ)ouw{&-Rb4fu&8ZM2b zbPLjofOMCHfPr)ff+*_u0KRg8|A&X8o@ZxvcD81BXMO`Z&_}P3b`CJUgWyullUN)| zwY@q6+f1?d0iodCvxQ)b%yK;j(H7a?q?r1z@wwe0)hJrMNctsd!SePH1nl{50V%;& z2noZn+1m;geS2T*qqUU4m=CZpO9R8`bKk&{(~OhX3?!sHSn_vNB*ikjYf`_DqZu6y z=|s!E^MfgxkdHqiMy9#OcudVC%PhOrvz+9MEq^;}HJA?5ncmuRRkKm1Q9g{OTU}G# ztWl)>{T%)h{Lf@kPHKf%E@G8zu7kq3AHrP$2Rxt3?jI9oy=ht?1b_eHwaU!dAz>Or zfZ}!})5M_UtoM7fBB8fl`+!Cfdv8}nq#mU_NtEj=bMFe}ejfX4LT4HwR6$~OB%B>@ zn3pS9eaT5ZA^|CTPE|`ewjfQC@~F|+4Kp@Mj3Y07JFQBoT&=?CqUi~R6||%p%>A<0 zHa=sFU3xg}qL|%5wqOTUA3AZCMj!BL9G?05bV`W$*fD1%En!*@sPI?)r0NMDSma>y zTRcJ#f=s@AOc@`&AU#K)EMDeAokON(8DZo@u?hL3){++gX*~JBA)=ScQ&hWiIEpVA zgn2cv1wN#?Z}oZmYmQ}XzAF}hH^i%xj=|Jyr}lv}&0NO=PqPvM`Y(LEH{BbmSDL?q}5F~UcEweaaNNrvIEs*~o&HCtPq znYP!}v}T$gpQ|$3#zz{|BQtue)4R@33A))}88q>$34eC!TJ+i5-QU2dezpDn^Jr=0 z!}QW|eR}Gjo?PpSU*75L9!RUJ+hR|xm3_7u%7l(+>R014 zeJI%Cy*Wn^>crb9k2|j3B>nmB(E13luN&~LdUKx7#wuYD#=rZcZeb8PIDLUpaiXB* zeGI~!l&uUw?ThC&wMcvw9zp8P>6-KHKihbyyL&_(pJ>o@9~}tq?#^_D<>y=UV3XE* z^v90IpYALs0^XY{<^I372iQ?~e&vgxo7M=>y}OHc8(qAKJK|nW#gOH_ND$e>x4p<} zDZ(`NSx>+z{xmriA$!@8MqmcL>NVNZX1&DB0ckI;UyOnFz2bomJktc2UZRjlK?Ij% z${#3g!s1KlnEoH`_$(uG@x9@gk7Gu4Rhu%FHzMOS%eLq6dg;%1;>}HNWYqNCpC{|q z>21--&*a3ILmaXo;$v!4Oi7)NsD^oh+9X7-A-`t$*myyrI`+uqd^|8`2<*|~yXX78 z-AC43RhW!2Q=dKy_Ik(gCM^+6QXz0nbiR@(lN}!#Lh~$g`<^jtRz+LH`o-vLSf6(K zL95m0GK!OcG%QZ~u*ZbOy*)mp+;(ft6#H_#jkr%_GI>#=t=mm~sw2VT<@b)Np0@{R z9Us)`6nd-0Vy-9E9QU<@i3JQ`YCkB46O2<(21vY=Wp|3oDjm}7?t?AzVi-V5p=^KB zsP<0F{Oy|^M0gQhifzU9(j9E=7~0hteK(0K(x&?({`t+J@%KK-=xXHQJy*m zg{J3j)5)_5b#pPA&$8e+w*;L|e_~aKx?ZUSu}!+$^2f6|96c31#2!4q))@LXA`K6?nkCjDeeBf&5{GSI?(ZJ`}AK>(|}J}o}3l0H}!$!!`eU8S<^LU-QzXX9fOVEhN9dasL zE{~q~lE>wYxC~KX8vIY}f(5E0{D>9Ah*O*!C{Nl1NHU~NB@CFEwMvFK-KLfi)I%_o zm-L?6?J8Hq-0x^USqU(RC6HN*K&FaZwazM{ruHE-=L!UEMle%hW?vE3)-h7_b^J#7wf0_yAB~G_ z$L%Cu%k(xHhOwIt&DqO87vqiz44bHVBf;%EZK61P{_!{r5^*@pfz(ur_FbG@P23uP z=!Y8pHPuX${^(9FPb<1l&CRWacypH|)DVqH6?mA0+4Gu)q+lY%)uh1ucot zX(VYe2HiCfh3|i5mdR>EPuYS3^NJ89I6?odjJ+Kz!e?qeRU@|IbqDXEg5ml$hgwEK z)W>>h(R@r#l4&Mp6AnB*lz#t7EtGUih+;zvunLXgwMt*zu&WA~th7Gz+Lv-5?~Yy= zINk zd}#u9t6s*0i9M(__E*V?VvaX|6Tad$Q9+=+s8u^pMDPaO2-~^!@xG7|T)z0#B!!-yv4aO|1?;#tYn!iH;+d4u8Q!iNNf z&Ac{~j|WrV!v+puw~{D=%0jnkL#;Pnv%wxq#pzB^yi|BOT5b6HJ(n|iP{N(8_N=V- zYMe071X@A-vd^fe8+{Rag#LFvjTrCmy+jHzo|{>lnqfQ*IK~h6Z^2r5#FmfQV@6o* zkosVg9j{7o=dpsPOlR_lIm4zc=~P?`gl1(c=dg<}>*3c(C&SLo_T1BV!#rv;q46xl zUIMBE$aD}UwTu8$$LEd^j9<-z#g2|otG}DEjs2>9f6^PX1?9vPAO1iacZDT<8`v{5 ziEi$hjr3BWI|w9aVKf8bYCCX1znmR+prftMw^n-v5Yhh^MiUS>FZbpwbm#!rENomH zN%k3%X4oV`s|u2AD?QZ!ZPwhB1GeXL@woPuj8MvAKETGC-Thc^J_9X)1JQ5{dKudrN6Mf4R=MUg0l_~xYdi56Sj8i*Rr?| z9FvFUf?MafqagxC$|ofFpI*5w6M8&XchOxN=xpD4Y1$e-iWI3eHb8N> zjf;vPNe~IYSG4xCWm~v$z*lDRfuEXJpn>(9fOBj|CnH;_xA@HfBqUyF0rA zUyy|T$D>hH=+&5m3T=YrU~m`;TonaEQMtY0SNrnf>@Wmzy;A!~^Ebf1eEdXel%@xD zWHeF|Sd4DHuR#^8#Mb}fi^CO@i@1a+s!X5_&tVhq=LC+}tRwT}>Zg&LDNmKw&`-1R zc83WZPWno+fu%2psU{HspETww-#KUGl)ZHKv{sQUDH#33j9Gt|t&)$_v{fu{mxIz&#L@t5K-ZtoMr<16H2 zl};+SF^CT66~GpM(ENjwT^b{Zi)ARjp!WJj*2(F@+EQuQ>=U!%rw_PpyXc%)k#T;9@X6!Jq`yTc4Y(ZMo2zA1yfcdV&%qYuN zeo_INUzz;v^%BF4FJ)7TLpRITTRwfA!c4a|!6^mf*#BzO4wf`j-$Ne}oSqMw6duL2 zv}Hvh?Od}&`q=l54;=Hcx)QM28Mp{&=8_|dUd8&%?Q}Q>Yif)z%+cc_2mlAlc&?u;t-Le|N}{V4kVoj@dI3Tu{KLAybd0NwkjEiQAVQyU7&ALL<< zdm&71Wj*clY4&$2$IC|Ij*3 zvr7H=W^g};-6!^Bb)zHbA;tsp#3(L)1>z=Z_q?sF_}`iz^{44D`5fd7Haw?XvSel# zoF@+5O1K`*tGU#`Ni7No!)Ms(Z0&eOjJCZ^zis=7#@SvuH#^0wdgv?r_uH2{gOsSU zHsj$i?lWxQ>}(DMY(k%%&z7IYd+H!bSQ6~QC)!&iqvBh@Vit^j)s^jhmCDfqp(EB= zoCo_s>a{>pU@hEGE6*dJRfo6L($AJ0$ougk*g!fy%R5obR7M&rU$J%f4(Pf-%EJM* zbQPT~UXLGsB|@9U(2YOTv2DX)PZ4wtSDoq~;yhIcwfZyWGH*71Br!#E+8I!BOlFf~ zE5z25*tZu8WR`q5Ca_)cvxT>zH|t&2QJ?wb&LI-9E_nXd#*-wx-xQgW4;J<1%{ZH~ zq6cGa7L`0JlH_!QSB_KlTEFMjAEqj+-pV`5EyW^@rJS8`itNQ`s8Z5y5>Udnj|CYIFHd>dVf6ksVH6-6oT_g)5d z#-6;7P++bld$7lj&N9uJ0*1MM(lkIbhW&}3ojGF!US9som!5EM-kZ;DY8^Z^M>1}! zj^s~}{(yPvfme8>Y)ha2+yUD_>9L#~T}k;JmhZo?g}0oD5D&Ffs$aim|9YPp?y2n& zPB$K7GY_~GF}SY?G_;z;X5RCvnXfOM4)nDE^|7=AL`v3W!5CSl2lgJ2w2& zvQB5wO^J5W<@0vu0AFB^j8CkRXT;MZrl-%G7fEcv?bcNi-*Q@3(b_1$i{K5EaF|HD z?LYXE2s-@xUO1-&vsFpo3P(76UwxH$u&wiYZg3)W8kxDUqP1YL!DWUuL-}N?*=^S9 zhpJMQcb{E&M#vB?g78o#))GFK?lz52CR{FyP+oV5{SwL_;ipNn_~>TuLOI|?=_1Gk zCgFm;z$7t*oi_P6(Ym|B_Zk<$*@-2{>w>hDeZ@TzRvRMz`Um0@7 zXZHTefTh$Wt}_^5H4}W&G((dW=qI?Yh_RFTLo2D6gp11=UWaqt26ru(ZWt?_A9m6t zDnfK|#Z7|TUEFjF$Kj5fhEo_8*pS=sSl3Uy66_>k&eb5ibT_aL32eJO_{^^>W6Ef46k)r~yUI+to%hs{Lhvqwur`o&;V!~-`GdrD~ zC$N%E5he1c4mGUS49RGb4!f2s`6`&-%6iGV9$}aAMhl)kb%IwCM!>0~AwKaG9aCiz zECsuqrj|RiXG(OF8Yq_TnaelVYX^~uz~l@ScZ4);-;3A}r{8evC$PQWa=_9$+k*rTCexH8(`Q$X3o=z!ip4^28#iupN;C^mVg;@e?^D66x^%tD(1o1OFpN7jzs=lYsksV3t zccHYh4FZGurUd)H%o?mJ7>~T?6f?v&f;*(w!Id$etU;KyLsk)d9{oWPLYLtsMVHqrQ%oM^DoAI#u7R&yyY@8=aQ1zPw zvnl195&9^2_rHjVxc4FmS!sMt8KXQ7K{s*>i>+R1zg+*p2Wmsk7oj>-_n*K|mA`GM zyQAs`?pFB&->&4wMaFoqREU?1!DI8Q&WD%J-+p52Xcuwd8IurZnSe0=-4%_R zfN;SXXJEhwOoFuBS@q^~9#bgD=BDroFpKp}=2&&Lsj=^r8f}Nb-WLs#M?x{x*DGr3u}bZx>Tm9#EJ9Fnp9n5 zO3^ilbV%6HH3`Ri8~-2&L&7l2jKNluwo8zhrA@$btX$b*%2!@Se$oM+hH+3uJW#~ zSBZPAarj9Y4%?uVVRUYWBV~0U!rPSE&udhdFMTVjGWyZZ{#$4-kqc)9r^f}SJWDU& zQEi#f$>O~i%LH4-h->L{Jej1dE^IIonjeQ7W{k%=8T4bX}r?>+d zF)L1}{PSL=8TK30nd|H@RUGE}{uXeM@&|sQ0!J>!p0fG>k4az-@C*p!RUkQZO49t)UbnI`r9TANCG{8Xp+3Lxg{X!BGakxSurb#H zGzLl}$0xi3nqgJn?|0S_162S!!;VW~NRuvN;Jc;j=OpdKNjvQbi^?7G}jEd4zdW>(Xb1 z&kDQXZ(Z8Na~jFo3ySyb#{r?70imE^)y{=-15oF|P`GMkh+!6kC}G#*0Fz^1M*)!i zk9K{ca=Xn4T9L_;rb6kXwmbXP9@Q(_S!5Dfa-vZE+)GP{M3 zxY8Gph?D*Ft}4mmk%4n>+t?m5EByCn{jv66A#&Dns)LeX&Zds4=_j!cln16@{a*^$ z*6%QI7-l4YD-P!{_Q0g!rdHqK2lxoN$=C^WIC+Lggi)_VxL^(RyXW5C7=B0oMcd}c^ z@J1#AB*;qq{+QAOZ1l;0_@ODQp4yS3WWsU(!*}BT;q*=+PS=R9VH9$cMppJj1KKE6;CvIlp^z3F6j`n0w2(Y5!B zCTs!8olKPW*9oz^xS3f2IA3djE5}Ku2r)XGW`{lt)-DFI%GyS&-zC_G8)F*ivL6xd zGj$%RxOV^Rkg8GG2TQzM%;AS0zQ^8HGwAImJJ8g6 zH)HjO(9{5aI1shr2#X00WZE8281&9hvZQtthm(=lU$%T9itl_;xqvcq1_l8zr~dt* zU78hVbIRf(lfx&V|JR(tfJ_eHGYE3LHaw(mfW!?LIk?ve-0oeDoBt1dJhsO14Lj-s zufT8$2bj#Fy@f)(N6#dBs6v@15KJQ68IZ&Ov^UHZil#VIzF!PRe6-gk>O6DwS=|3O z*m<|keaAL$En!d84A@XnM@Ug`<@j{C95#L9t@aQ*UiA#BEVx~f(0w9M>Z3y17-jHg zoZ5VXkGG1N;uQw|5A#j z+f}7}1{8Wdoyb!Tl(XQ-<6|Z=Gy_uIv3I%f-KvopxU%yz#F2^$hL5Ozdy#;7-@bQ8 z^jf-IBTlhbfR>vHJAq?ork7+dWMcJPEOv}(+B1Y(0md2Sv8uU5FY)p_EHZX@=P2^N zFT2eNZ+{jxXN&l}W6}bEp4gsdFI3fr8X``mb6JqDW9YtIleRV9JB;G;a5HsuiOoQ5 zzg%0+8mKu$^HK#2SdgQ@g##1@^2IoDbFsH^hgyOEzpmHVc1jl|{1XW7z8C3YKsv?s zbJo82OMlcT%#bayS*3IH0OlSa|4{RvfVUaXJZ%OLVMp9LAU}naHKuq4W#GZbEq!qC z+Fh9;Ub{;9$Bq=Y?P;w`=j7iOj0cRB;Bw2acuXsq;Jiu7QtIn_wxLT92bG z%?)?Ohu_I{u;DU`)?Iwahw6Ud$&}vp2&^@mqs{l{l@8BbU?#}${v>vL=<()j(!qk5 zzz?$%mbD_4)uFkjKTw;wD-l&4b3-;%NOEcWLN?g}SOR#Ljk>Tg{moI^+m(4tsvcVK z)f-&Tn*E{#s*R|B?qD}l%1eR??5&uO?fIBC&9-9F)7Exm#A1``KJRGqd%WeU1e}r# zynPe$zS4o0?Bv(O>bO^0QhZ^vh{^mDX69+Bk#%byA7xhWh=Z2w-`L`bSF`xEHhVm5 zPeRAp?h@Z&n$#WBO8&8|s_^t_zpc@4j&3yp+Y!?}SAL^XZ@Khf*XACuY>&2kOiE^d zc+9COLcq%)4CEi@$ZQvT+SxeO;7DLv0OnyWFcG0}?k%wN8WNejSC=+7y;TUfHbb0o!>M-?RO21Ok_V z(|9X?MdmOe<$MA2n!bIwnNK!B2IWK*3F&1=aVcrep;l-*r?I|<6@x!=xBIz~-NkCE z7uo!E8&`kVaKkSxcN~#%$1r~}Iuw5U0CYb=fyaLrZD3xKG1VbjIwdy*EYqga%!izS z(sf_cym?%MfHB6lP;c~gAkWmpbN~V?mmg>0@NN5Mv$L;5ge5L|_{jTj4G2%Cr9;%F z+JDZm%F=!ZU7gl43_Q4rz^pC=B2u6*_Ak`s?BwQd6X34{m zQ#M=u2Vn$k`2AtbauFiv?fI$JuF@K})zkN+sfHd@7H?q_p{j1E_P#8pWs!Dt! zGKkys5&m(?{l#_1XFB4Nl@jK|S>wfA-27aVEzg^fu@vp5;a8%lI-9%~tkz*ynBT`1 zPuea@9I>V-AdyiCP%8ffzbJj&eCz3>DvF2w(UKX$WAroOhfj5~e9HGp1z;Y%y6?%M zplrMfA5>KoxD5N4D*k!T;7k%VF32D6Rky$I&S2FFs?l+7RGIxDWf5ZnGgIbdva174 zDMgk+vm^PATkdTIE12KmU)W#M4HuHUs&?w)@_XjI<-_V}xjAe-vH|Ycc4v^xUZmw} z%6EO^$fVEh)NR@_c3(AoYjCGcK8rSNU)x-kGy|=8`zwEX)(`I9;=2M10kJ!7jM|ip zG(+JBHBa8cXzYcw>J0Z7rhivneect<*wL|1Dlv5E12=3xZlB+LjBlIZkUYmHUpoYE z&!Hr0bAYG3o&h!Yh|8Km0I6x0+6Br2QsFq(Mb!;VV}VL?gqB@!lVIIHBRB zofnf90U^I!S}N4mg5??>w#gT-znr*-+Qug29muLN>4uZ2a z`uC)GuAMc@P=o#$45Ccm37zM!UT383|5#}Kp{_HCz{ZT*{@wWQE*N;daD<=wQQU?; zM4(2vU*8flHh*$z59G*^s`6*5(`U7b#7{_{&zAbk_{jOz67wO{o2zt8vUb#?H2)4@ zb;tl#2lDH-i-o<3o4bvfn}vz18Pp-C+dj=)Rv!MC6@fW^F4dLxSZVE1>_)Xsee zI66j`Fmfc`q|kTVT?u_s4HEMmY$z3BahRn}2R--FlPdXSR>Vd#4@8T7kYkl42wNG$ z^}lEb>fvQ=qRu4I23gtN$&gw4e56Sxz4uXkMfmhXbtxBr6I;W)W1(eIN&4=NBlySf z37&EC5+s!L?XnbvrC2NfU@Wzi=9HL`K(BZ#WRiB7jOFHWBLQAk9#GU|z}o{vWC&|w z^DoH9rco2P3>A65cKAX`J~JGl%xeaW_aiqo2(S=D0Sm#6b<@G;VhsTQ%_H^yt^o#4 zz|B#>an(y$MVlgKIl!+-erl}CK)R)UgMFq~M;Da2;h)FeIxBw&;zCr;$ID&YJj z{na1$R`~yuJDMQt zO0wCbkL#}emfh<1XUTR^jId--kMy``uYEr!jRlLfPKFB^TU=V)+vEPIB&j*XlJuUt zcoyqQhsCh!bu~#0jEP|3@#5YWMRpc7zR@MI6_^s%S^R`s_ng}>_t*rGU@26)o^U~& z;}XI;F$Qw7^?twpRE@FZ@8@`;?HpW%Kw(2|{X2;~7G2~JvATd0n|UJLQ~fhSvvc}n zn|mhA0-hk9aSP?|kbu;cdz<68O}JD{T|JdoCJ#vMo0QXBmIzP5;Di^;lAej9pHiZ} z^w^w~rIWP(kZ-@|9X?^R-XVjci1g$SO9(T>cE9$;_MSR#o83}fsV!>Z?X)LtA6uA; zXPH$>#Ig*7?g*mgZcOeCsWnc|+J6~5Ufp&%6?`lFh019-Bu>8NhQHgiHw5L~3Rc34{aBBUgEV-CNlWJ!9CxY+%4h~v>7Kr(p)#2^TXMhsOE8_-W>kd1n=%>omu8ww z`6>Krf8z9Ao-YPO8*PnRIjD9ZkdXKhWaF7C5?Ip|H-F<|gm8r1ZoM}OwGNzF37hxv zRVRUU;0z)cDtFq&wrA<&;b?B+ZsX)=ZhF;l2meoN0XWW3U_LtbLx$6%nSWdwV^GS- zHd^!5`H@>l*lQtl?!cd-YMK?3=q4Y3#_ozlH+>9JjnK%G*k@>OFXok1=r7{?X|}=? z?&puNEW|5a^~I?w;}EH~q9A+93Dr{aKDS`p?1~l7pyP%Em|iE6n=i)&S&>2?>DB4G zaTK#b>v<2jyMehH1?xGk+oss>o0kceW&;r|U%Pqb$ABCiT$gAG>gmW!4k~k*35DYe z5ndn*$k(ub!(;vC5YAil=OnNLUw9np-h_0nb(NyPw_-9b{cCGOm<;9XEL#j z`!KQ*HtT~MmoK>Nvx(arOYFRr(?bTW+ZsJgi#@{Xuap;F-L0#6%C>B4U#j<9-6`A5TTBf2 z=ikhfkmvRw28NrdCohTvTE0~f9C2AZQ4ALs0pJegqCDeH3ZZuZVG2(isro;r<3EsDxoqoX*<k zAc8vmQ${*wm!lb=AO{4&K=D!r@ zNi)s|BzYmvMbF<%3}3rxU%Ingg0n2>1_lozRIofW86}b)0j7 z___eLi4oRy=yQjVYfN;!hW zU49hE+F3dJE9EeNCH=q71>o5=iCC|EW{kS>`5dQRf{5u#8H}WVWdOi-8HSr!?a*44 zdj7BU8FD-M8EpGShZcJQ>SXxK`fSfB2)3&ES2(e)g&MNsbweSfJ23wmaHfzmpmwoq zdi@1e20(0A`&Y}wzVl-D3Ja}&Blx1z_+dc*YB={x^7R36U8^DHY8eX1vJW6B^%Xsh zg0AraAl^|S622fRhMO?&&?-ZPyCei&#%a#>Q2_V-Tx$$BA=9BrN#QR^&t(Sy)G^QK z0SxsE`u|Y1FG01T=~WRg=>dps@-tEZW&48kCaNVgDL&>U>3P)v#wFevz~u$;O*}qm z;tHH=!~iVcx!eFU-=*AFq3fXOr*N;)XZeG$&Lyw$2Qd@TUg;Q5{A+~)s5Nv5W&qGJ zH{sTxYxzxhNeFbzIrtj#nTmjLunQ?~!lgjdhf!Xk$2)^e!MH+yGYB1;z5)P?`)~4| zs~8ALKc@$x(l6@28T1HEAI*A=9teTNI*WfiCx*NY1SaIwG2F|3jRFXnV?Ec>)nGa3 zS|T|wX#gz|U_ph>UDwssfFNitlsuPQ&f5xv2ccfk)lH9dXnJ_TOL{-eJgPJ@BoyIR|!+$GU@vp7OP91J)8 zFrh25Qo2S1cx9rTwFq#`yr`%v1gMDW%8aVkxFiCcd+tDBLvQRn37VH>fEJwl`>>x6 zrK_Gk(846@T+*C30Py=kyD|uFdTT(_a{$Tsu1pxfcjL-(2+zkE_+b5y??tCyT9JT5z?n1`h8*wBN8}4O7yJK5+kb2>t_#k46L?ZS zZ;$ZVf$ZvoGPDegcGrmiJud^g{O>4s&g1I)D>M%w`)fS@y~aZE5I>XS{D~7d(O$f8 zLi4!eaE-^6_f9B2VrP7=K0!hA>2SKn2l_z@ijCA6o2ySV&}_Dxud%uDPy@wF_U64e zG_NVQYrOu~EjYjqxB)-!vGW#Qy)1@ix9xt7-G5idXE(=CQpudP@aiobG@B`}Yi$0< zog5T5n=@|zZmEEU=js%J@wvv$^~#mi1*7XP@+$K1z*YFITROm>msG%;w=Xa>Zv7u% Cik`Lr delta 28008 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 W00BKPFbcqbabSD$$`m-a!2BPj{