Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate extension files #844

Merged
merged 4 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[package]
name = "pg-trunk"
version = "0.15.5"
version = "0.15.6"
edition = "2021"
authors = ["Ian Stanton", "Vinícius Miguel"]
authors = ["Ian Stanton", "Vinícius Miguel", "David E. Wheeler"]
description = "A package manager for PostgreSQL extensions"
homepage = "https://pgt.dev"
license = "Apache-2.0"
Expand Down
117 changes: 114 additions & 3 deletions cli/src/commands/containers.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{bail, Context};
use anyhow::{bail, ensure, Context};
use bollard::container::{
CreateContainerOptions, DownloadFromContainerOptions, StartContainerOptions,
};
Expand All @@ -7,6 +7,7 @@ use bollard::service::ExecInspectResponse;
use bollard::Docker;
use std::borrow::Cow;
use std::collections::HashMap;
use std::ops::Not;

use bollard::container::Config;
use bollard::image::BuildImageOptions;
Expand Down Expand Up @@ -164,9 +165,10 @@ pub async fn run_temporary_container(
}

pub struct ExtensionFiles {
/// Files stored in `pg_config --sharedir`
/// Files stored in `pg_config --sharedir`. Should store architecture-independent files, such as SQL files.
sharedir: Vec<String>,
/// Files stored in `pg_config --pkglibdir`
/// Files stored in `pg_config --pkglibdir`. Should store architecture-dependent files, such as
/// shared objects.
pkglibdir: Vec<String>,
/// The parsed contents of the extension's control file, if it exists
control_file: Option<ControlFile>,
Expand Down Expand Up @@ -228,6 +230,8 @@ pub async fn find_installed_extension_files(
.any(|pattern| pattern.matches(&file_added));

if file_added.ends_with(".so")
|| file_added.ends_with(".dll")
|| file_added.ends_with(".dylib")
|| file_added.ends_with(".bc")
|| file_added.ends_with(".sql")
|| file_added.ends_with(".control")
Expand Down Expand Up @@ -454,6 +458,7 @@ pub async fn package_installed_extension_files(

let extension_files =
find_installed_extension_files(&docker, container_id, &inclusion_patterns).await?;
validate_extension_files(&extension_files)?;
let license_files = find_license_files(&docker, container_id).await?;

let sharedir_list = extension_files.sharedir;
Expand Down Expand Up @@ -622,6 +627,39 @@ pub async fn package_installed_extension_files(
Ok(())
}

/// Validates the presence of essential files in a PostgreSQL extension.
///
/// Returns an error if none of these files are found, indicating a potential issue
/// with the build or `install_command` used.
fn validate_extension_files(extension_files: &ExtensionFiles) -> anyhow::Result<()> {
let has_control_file = extension_files.control_file.is_some();
let has_shared_object = extension_files.pkglibdir.iter().any(|filename| {
filename.ends_with(".so") || filename.ends_with(".dll") || filename.ends_with(".dylib")
});
let has_sql = extension_files
.pkglibdir
.iter()
theory marked this conversation as resolved.
Show resolved Hide resolved
.chain(extension_files.sharedir.iter())
.any(|filename| filename.ends_with(".sql"));

// Base check: should at least have one of the three
ensure!(
has_control_file || has_shared_object || has_sql,
"Did not find a control file, a shared object or a SQL file. There is likely a problem with `install_command`."
);

if has_control_file {
ensure!(
has_sql,
"Extension has a control file but no SQL files, which is invalid."
)
} else {
ensure!(has_shared_object && has_sql.not(), "Extension has no control file, therefore it should contain a shared object and no SQL files.");
}

Ok(())
}

/// Assumes `file_to_package.starts_with(sharedir)`.
fn prepare_sharedir_file<'p>(
sharedir: &str,
Expand Down Expand Up @@ -742,3 +780,76 @@ pub async fn start_postgres(docker: &Docker, container_id: &str) -> anyhow::Resu

Ok(())
}

#[cfg(test)]
mod tests {
use crate::control_file::ControlFile;

use super::{validate_extension_files, ExtensionFiles};

#[test]
fn validates_extension_files() {
// Valid: Contains files of the three kinds
validate_extension_files(&ExtensionFiles {
sharedir: vec![
"pg_stat_statements--1.0--1.1.sql".into(),
"pg_stat_statements--1.1--1.2.sql".into(),
],
pkglibdir: vec!["pg_stat_statements.so".into()],
control_file: Some(ControlFile {
directory: None,
module_pathname: Some("'$libdir/pg_stat_statements'".into()),
requires: None,
}),
})
.unwrap();

// Valid: Extension with a control file and SQL files, but no shared objects
validate_extension_files(&ExtensionFiles {
sharedir: vec!["pgmq--1.4.4--1.4.5.sql".into(), "pgmq--1.4.5.sql".into()],
pkglibdir: vec![],
control_file: Some(ControlFile {
directory: None,
module_pathname: Some("'$libdir/pgmq'".into()),
requires: None,
}),
})
.unwrap();

// Valid: Extension with a shared object but no control file
validate_extension_files(&ExtensionFiles {
sharedir: vec![],
pkglibdir: vec!["auth_delay.so".into()],
control_file: None,
})
.unwrap();

// Invalid: Control file and no SQL file
validate_extension_files(&ExtensionFiles {
sharedir: vec![],
pkglibdir: vec!["nonesuch.so".into()],
control_file: Some(ControlFile {
directory: None,
module_pathname: Some("'$libdir/nonesuch'".into()),
requires: None,
}),
})
.unwrap_err();

// Invalid: SQL files and no control file
validate_extension_files(&ExtensionFiles {
sharedir: vec!["nonesuch--1.4.4--1.4.5.sql".into(), "nonesuch.sql".into()],
pkglibdir: vec![],
control_file: None,
})
.unwrap_err();

// Invalid: None of the three file kinds
validate_extension_files(&ExtensionFiles {
sharedir: vec![],
pkglibdir: vec![],
control_file: None,
})
.unwrap_err();
}
}
1 change: 1 addition & 0 deletions cli/src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Extension {
pub struct Download {
pub link: String,
pub pg_version: u8,
#[expect(unused)]
pub platform: String,
pub sha256: String,
}
Loading