diff --git a/moss/src/db.rs b/moss/src/db.rs index 6283ff52..29bbe9b1 100644 --- a/moss/src/db.rs +++ b/moss/src/db.rs @@ -4,6 +4,7 @@ pub use self::encoding::{Decoder, Encoding}; +pub mod layout; pub mod meta; pub mod state; diff --git a/moss/src/db/layout.rs b/moss/src/db/layout.rs new file mode 100644 index 00000000..86a05432 --- /dev/null +++ b/moss/src/db/layout.rs @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: Copyright © 2020-2023 Serpent OS Developers +// +// SPDX-License-Identifier: MPL-2.0 + +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::{Pool, Sqlite}; +use stone::payload; +use thiserror::Error; + +use crate::package; +use crate::Installation; + +use super::Encoding; + +#[derive(Debug)] +pub struct Database { + pool: Pool, +} + +impl Database { + pub async fn new(installation: &Installation) -> Result { + let path = installation.db_path("layout"); + + let options = sqlx::sqlite::SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true) + .read_only(installation.read_only()) + .foreign_keys(true); + + Self::connect(options).await + } + + async fn connect(options: SqliteConnectOptions) -> Result { + let pool = sqlx::SqlitePool::connect_with(options).await?; + + sqlx::migrate!("src/db/layout/migrations") + .run(&pool) + .await?; + + Ok(Self { pool }) + } + + pub async fn all(&self) -> Result, Error> { + let layouts = sqlx::query_as::<_, encoding::Layout>( + " + SELECT package_id, + uid, + gid, + mode, + tag, + entry_type, + entry_value1, + entry_value2 + FROM layout; + ", + ) + .fetch_all(&self.pool) + .await?; + + Ok(layouts + .into_iter() + .filter_map(|layout| { + let encoding::Layout { + package_id, + uid, + gid, + mode, + tag, + entry_type, + entry_value1, + entry_value2, + } = layout; + + let entry = encoding::decode_entry(entry_type, entry_value1, entry_value2)?; + + Some(( + package_id.0, + payload::Layout { + uid, + gid, + mode, + tag, + entry, + }, + )) + }) + .collect()) + } + + pub async fn add(&self, package: package::Id, layout: payload::Layout) -> Result<(), Error> { + self.batch_add(vec![(package, layout)]).await + } + + pub async fn batch_add( + &self, + layouts: Vec<(package::Id, payload::Layout)>, + ) -> Result<(), Error> { + sqlx::QueryBuilder::new( + " + INSERT INTO layout + ( + package_id, + uid, + gid, + mode, + tag, + entry_type, + entry_value1, + entry_value2 + ) + ", + ) + .push_values(layouts, |mut b, (id, layout)| { + let payload::Layout { + uid, + gid, + mode, + tag, + entry, + } = layout; + + let (entry_type, entry_value1, entry_value2) = encoding::encode_entry(entry); + + b.push_bind(id.encode().to_owned()) + .push_bind(uid) + .push_bind(gid) + .push_bind(mode) + .push_bind(tag) + .push_bind(entry_type) + .push_bind(entry_value1) + .push_bind(entry_value2); + }) + .build() + .execute(&self.pool) + .await?; + + Ok(()) + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("database error: {0}")] + Sqlx(#[from] sqlx::Error), + #[error("migration error: {0}")] + Migrate(#[from] sqlx::migrate::MigrateError), +} + +mod encoding { + use sqlx::FromRow; + use stone::payload; + + use crate::{db::Decoder, package}; + + #[derive(FromRow)] + pub struct Layout { + pub package_id: Decoder, + pub uid: u32, + pub gid: u32, + pub mode: u32, + pub tag: u32, + pub entry_type: String, + pub entry_value1: Option, + pub entry_value2: Option, + } + + pub fn decode_entry( + entry_type: String, + entry_value1: Option, + entry_value2: Option, + ) -> Option { + use payload::layout::Entry; + + match entry_type.as_str() { + "regular" => { + let hash = entry_value1?.parse::().ok()?; + let name = entry_value2?; + + Some(Entry::Regular(hash, name)) + } + "symlink" => Some(Entry::Symlink(entry_value1?, entry_value2?)), + "directory" => Some(Entry::Directory(entry_value1?)), + "character-device" => Some(Entry::CharacterDevice(entry_value1?)), + "block-device" => Some(Entry::BlockDevice(entry_value1?)), + "fifo" => Some(Entry::Fifo(entry_value1?)), + "socket" => Some(Entry::Socket(entry_value1?)), + _ => None, + } + } + + pub fn encode_entry( + entry: payload::layout::Entry, + ) -> (&'static str, Option, Option) { + use payload::layout::Entry; + + match entry { + Entry::Regular(hash, name) => ("regular", Some(hash.to_string()), Some(name)), + Entry::Symlink(a, b) => ("symlink", Some(a), Some(b)), + Entry::Directory(name) => ("directory", Some(name), None), + Entry::CharacterDevice(name) => ("character-device", Some(name), None), + Entry::BlockDevice(name) => ("block-device", Some(name), None), + Entry::Fifo(name) => ("fifo", Some(name), None), + Entry::Socket(name) => ("socket", Some(name), None), + } + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use stone::read::Payload; + + use super::*; + + #[tokio::test] + async fn create_insert_select() { + let database = + Database::connect(SqliteConnectOptions::from_str("sqlite::memory:").unwrap()) + .await + .unwrap(); + + let bash_completion = include_bytes!("../../../test/bash-completion-2.11-1-1-x86_64.stone"); + + let mut stone = stone::read_bytes(bash_completion).unwrap(); + + let payloads = stone + .payloads() + .unwrap() + .collect::, _>>() + .unwrap(); + let layouts = payloads + .iter() + .filter_map(Payload::layout) + .flatten() + .cloned() + .map(|layout| (package::Id::from("test".to_string()), layout)) + .collect::>(); + + let count = layouts.len(); + + database.batch_add(layouts).await.unwrap(); + + let all = database.all().await.unwrap(); + + assert_eq!(count, all.len()); + } +} diff --git a/moss/src/db/layout/migrations/20230919225204_init.sql b/moss/src/db/layout/migrations/20230919225204_init.sql new file mode 100644 index 00000000..58cdd00b --- /dev/null +++ b/moss/src/db/layout/migrations/20230919225204_init.sql @@ -0,0 +1,13 @@ +-- Add migration script here + +CREATE TABLE IF NOT EXISTS layout ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + package_id INTEGER NOT NULL, + uid INTEGER NOT NULL, + gid INTEGER NOT NULL, + mode INTEGER NOT NULL, + tag INTEGER NOT NULL, + entry_type TEXT NOT NULL, + entry_value1 TEXT NULL, + entry_value2 TEXT NULL +);