diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 610d90db3..47f55e4c1 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -97,6 +97,7 @@ mod m0000760_product_status_index; mod m0000780_alter_source_document_time; mod m0000790_alter_sbom_alter_document_id; mod m0000800_alter_product_version_range_scheme; +mod m0000810_fix_get_purl; pub struct Migrator; @@ -201,6 +202,7 @@ impl MigratorTrait for Migrator { Box::new(m0000780_alter_source_document_time::Migration), Box::new(m0000790_alter_sbom_alter_document_id::Migration), Box::new(m0000800_alter_product_version_range_scheme::Migration), + Box::new(m0000810_fix_get_purl::Migration), ] } } diff --git a/migration/src/m0000810_fix_get_purl.rs b/migration/src/m0000810_fix_get_purl.rs new file mode 100644 index 000000000..02a0cfd05 --- /dev/null +++ b/migration/src/m0000810_fix_get_purl.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared(include_str!("m0000810_fix_get_purl/get_purl.sql")) + .await + .map(|_| ())?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared(include_str!("m0000740_ensure_get_purl_fns/get_purl.sql")) + .await?; + + manager + .get_connection() + .execute_unprepared( + r#" +DROP FUNCTION IF EXISTS encode_uri_component; +"#, + ) + .await?; + + Ok(()) + } +} diff --git a/migration/src/m0000810_fix_get_purl/get_purl.sql b/migration/src/m0000810_fix_get_purl/get_purl.sql new file mode 100644 index 000000000..dd29899d2 --- /dev/null +++ b/migration/src/m0000810_fix_get_purl/get_purl.sql @@ -0,0 +1,49 @@ +CREATE OR REPLACE FUNCTION encode_uri_component(text) RETURNS text AS $$ + SELECT string_agg( + CASE + WHEN bytes > 1 or c !~ '[0-9a-zA-Z_.!~*''()-]+' THEN + regexp_replace(encode(convert_to(c, 'utf-8')::bytea, 'hex'), '(..)', E'%\\1', 'g') + ELSE + c + END, + '' + ) + FROM ( + SELECT c, octet_length(c) bytes + FROM regexp_split_to_table($1, '') c + ) q; +$$ LANGUAGE sql IMMUTABLE STRICT ; + +CREATE OR REPLACE FUNCTION get_purl(qualified_purl_id UUID) +RETURNS TEXT AS $$ +DECLARE +result TEXT; +BEGIN +SELECT + COALESCE( + 'pkg:' || bp.type || + '/' || COALESCE(bp.namespace, '') || '/' || + bp.name || + '@' || vp.version || + CASE + WHEN qp.qualifiers IS NOT NULL AND qp.qualifiers <> '{}'::jsonb THEN + '?' || ( + SELECT string_agg(key || '=' || encode_uri_component(value), '&') + FROM jsonb_each_text(qp.qualifiers) + ) + ELSE + '' + END, + qualified_purl_id::text + ) +INTO result +FROM + qualified_purl qp + LEFT JOIN versioned_purl vp ON vp.id = qp.versioned_purl_id + LEFT JOIN base_purl bp ON bp.id = vp.base_purl_id +WHERE + qp.id = qualified_purl_id; + +RETURN result; +END; +$$ LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; diff --git a/migration/tests/getpurl.rs b/migration/tests/getpurl.rs index 9c2c17158..f3e656c85 100644 --- a/migration/tests/getpurl.rs +++ b/migration/tests/getpurl.rs @@ -54,7 +54,7 @@ async fn test_getpurl(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { match get_purl(&ctx.db, sbom_package_purl_ref.qualified_purl_id.to_string()).await { Ok(Some(purl)) => { let parse_purl: Purl = Purl::from_str(purl.as_str())?; - assert!(parse_purl.name == sbom_node_name); + assert_eq!(parse_purl.name, sbom_node_name); } Ok(None) => panic!("getpurl() test should match"), Err(e) => panic!("error testing getpurl() pg function. {}", e), @@ -63,3 +63,26 @@ async fn test_getpurl(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { Ok(()) } + +#[test_context(TrustifyContext)] +#[test(tokio::test)] +async fn getpurl_urls(ctx: &TrustifyContext) -> Result<(), anyhow::Error> { + let qualified = ctx + .graph + .ingest_qualified_package( + &Purl::from_str("pkg:maven/org.foo.bar/foo-bar@1.0?key=value%3dvalue")?, + &ctx.db, + ) + .await?; + + let id = qualified.qualified_package.id; + + let result = get_purl(&ctx.db, id.to_string()).await?; + + assert_eq!( + result.as_deref(), + Some("pkg:maven/org.foo.bar/foo-bar@1.0?key=value%3dvalue") + ); + + Ok(()) +} diff --git a/migration/tests/tests.rs b/migration/tests/tests.rs index 2d498f6ba..052022b36 100644 --- a/migration/tests/tests.rs +++ b/migration/tests/tests.rs @@ -24,5 +24,7 @@ async fn test_migrations(ctx: TrustifyContext) -> Result<(), anyhow::Error> { #[test_context(TrustifyContext, skip_teardown)] #[test(tokio::test)] async fn only_up_migration(_ctx: TrustifyContext) -> Result<(), anyhow::Error> { + // The initialization of the database will already call the `up` function. So we + // don't need any extra code here Ok(()) } diff --git a/modules/analysis/src/endpoints.rs b/modules/analysis/src/endpoints.rs index 3e917dcd0..43fa9056f 100644 --- a/modules/analysis/src/endpoints.rs +++ b/modules/analysis/src/endpoints.rs @@ -284,7 +284,7 @@ mod test { ); assert_eq!( response["items"][0]["ancestors"][0]["purl"], - "pkg:maven/com.redhat.quarkus.platform/quarkus-bom@3.2.11.Final-redhat-00001?type=pom&repository_url=https://maven.repository.redhat.com/ga/" + "pkg:maven/com.redhat.quarkus.platform/quarkus-bom@3.2.11.Final-redhat-00001?type=pom&repository_url=https%3a%2f%2fmaven.repository.redhat.com%2fga%2f" ); assert_eq!(&response["total"], 2); diff --git a/modules/analysis/src/service.rs b/modules/analysis/src/service.rs index aac3e980c..bae2999a1 100644 --- a/modules/analysis/src/service.rs +++ b/modules/analysis/src/service.rs @@ -1109,7 +1109,7 @@ mod test { .unwrap(); assert_eq!(analysis_graph.items.last().unwrap().ancestors.last().unwrap().purl, - "pkg:maven/com.redhat.quarkus.platform/quarkus-bom@3.2.12.Final-redhat-00002?type=pom&repository_url=https://maven.repository.redhat.com/ga/".to_string() + "pkg:maven/com.redhat.quarkus.platform/quarkus-bom@3.2.12.Final-redhat-00002?type=pom&repository_url=https%3a%2f%2fmaven.repository.redhat.com%2fga%2f".to_string() ); assert_eq!( analysis_graph