diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..740a0d5 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,173 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + CARGO_INCREMENTAL: "false" + +jobs: + Test: + strategy: + fail-fast: false # We want all of them to run, even if one fails + matrix: + os: [ "ubuntu-latest" ] + pg: [ "12", "13", "14", "15", "16" ] + + runs-on: ${{ matrix.os }} + env: + RUSTC_WRAPPER: sccache + SCCACHE_DIR: /home/runner/.cache/sccache + RUST_TOOLCHAIN: ${{ matrix.rust || 'stable' }} + steps: + - uses: actions/checkout@v4 + - name: Set up prerequisites and environment + run: | + sudo apt-get update -y -qq --fix-missing + + echo "" + echo "----- Install sccache -----" + mkdir -p $HOME/.local/bin + curl -L https://github.com/mozilla/sccache/releases/download/v0.2.15/sccache-v0.2.15-x86_64-unknown-linux-musl.tar.gz | tar xz + mv -f sccache-v0.2.15-x86_64-unknown-linux-musl/sccache $HOME/.local/bin/sccache + chmod +x $HOME/.local/bin/sccache + echo "$HOME/.local/bin" >> $GITHUB_PATH + mkdir -p /home/runner/.cache/sccache + echo "" + + echo "----- Set up dynamic variables -----" + cat $GITHUB_ENV + echo "" + + echo "----- Install system dependencies -----" + sudo apt-get install -y \ + build-essential \ + llvm-14-dev libclang-14-dev clang-14 \ + gcc \ + libssl-dev \ + libz-dev \ + make \ + pkg-config \ + strace \ + zlib1g-dev + echo "" + echo "----- Print env -----" + env + echo "" + + - name: Install release version of PostgreSQL + run: | + echo "----- Set up PostgreSQL Apt repository -----" + sudo apt-get install -y wget gnupg + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + sudo apt-get update -y -qq --fix-missing + echo "" + + sudo apt-get install -y \ + postgresql-${{ matrix.pg }} \ + postgresql-server-dev-${{ matrix.pg }} + + echo "" + echo "----- pg_config -----" + pg_config + echo "" + - name: Set up PostgreSQL permissions + run: sudo chmod a+rwx `/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config --pkglibdir` `/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config --sharedir`/extension /var/run/postgresql/ + + - name: Cache cargo registry + uses: actions/cache@v4 + continue-on-error: false + with: + path: | + ~/.cargo/registry + ~/.cargo/git + key: tests-${{ runner.os }}-${{ hashFiles('**/Cargo.lock', '.github/workflows/tests.yml') }} + + - name: Cache sccache directory + uses: actions/cache@v4 + continue-on-error: false + with: + path: /home/runner/.cache/sccache + key: pgrx-tests-sccache-${{ runner.os }}-${{ hashFiles('**/Cargo.lock', '.github/workflows/tests.yml') }} + + - name: Start sccache server + run: sccache --start-server + + - name: Print sccache stats (before run) + run: sccache --show-stats + + - name: Install cargo-pgrx + run: | + PGRX_VERSION=$(cargo metadata --format-version 1 | jq -r '.packages[]|select(.name=="pgrx")|.version') + cargo install --locked --version=$PGRX_VERSION cargo-pgrx --debug --force + cargo pgrx init --pg${{ matrix.pg }} /usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config + - name: Run tests + run: echo "\q" | cargo pgrx run pg${{ matrix.pg }} && cargo test --no-default-features --features pg${{ matrix.pg }} + + - name: Build + run: cargo pgrx package --features pg${{ matrix.pg }} --pg-config /usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config + + - name: Archive production artifacts + uses: actions/upload-artifact@v4 + with: + name: typeid-${{matrix.pg}} + path: | + target/release/typeid-pg${{ matrix.pg }} + # Attempt to make the cache payload slightly smaller. + - name: Clean up built PGRX files + run: | + cd target/debug/deps/ + for built_file in $(find * -type f -executable -print | grep -v "\.so$"); do + base_name=$(echo $built_file | cut -d- -f1); + for basefile in "$base_name".*; do + [ -f "$basefile" ] || continue; + echo "Removing $basefile" + rm $basefile + done; + echo "Removing $built_file" + rm $built_file + done + - name: Stop sccache server + run: sccache --stop-server || true + Install: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install PostgreSQL headers + run: | + sudo apt-get update + sudo apt-get install postgresql-server-dev-14 + - name: Install cargo-pgrx + run: | + PGRX_VERSION=$(cargo metadata --format-version 1 | jq -r '.packages[]|select(.name=="pgrx")|.version') + cargo install --locked --version=$PGRX_VERSION cargo-pgrx --debug --force + cargo pgrx init --pg14 $(which pg_config) + - name: Install TypeID/pgrx + run: | + cargo pgrx install --no-default-features --release --sudo + - name: Start PostgreSQL + run: | + sudo systemctl start postgresql.service + pg_isready + # superuser (-s), can create databases (-d) and roles (-r), no password prompt (-w) named runner + sudo -u postgres createuser -s -d -r -w runner + - name: Verify install + run: | + createdb -U runner runner + psql -U runner -c "create extension typeid;" + psql -U runner -c "select typeid_generate('user');" + rustfmt: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run rustfmt + run: cargo fmt -- --check \ No newline at end of file diff --git a/.github/workflows/clippy.yaml b/.github/workflows/clippy.yaml new file mode 100644 index 0000000..289ebe2 --- /dev/null +++ b/.github/workflows/clippy.yaml @@ -0,0 +1,86 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# rust-clippy is a tool that runs a bunch of lints to catch common +# mistakes in your Rust code and help improve your Rust code. +# More details at https://github.com/rust-lang/rust-clippy +# and https://rust-lang.github.io/rust-clippy/ + +name: rust-clippy analyze + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '20 14 * * 5' + +jobs: + rust-clippy-analyze: + name: Run rust-clippy analyzing + runs-on: ubuntu-latest + + strategy: + matrix: + pg: [ "16" ] + permissions: + contents: read + security-events: write + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 + with: + profile: minimal + toolchain: stable + components: clippy + override: true + + - name: Install release version of PostgreSQL + run: | + echo "----- Set up PostgreSQL Apt repository -----" + sudo apt-get install -y wget gnupg + sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' + wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - + sudo apt-get update -y -qq --fix-missing + echo "" + + sudo apt-get install -y \ + postgresql-${{ matrix.pg }} \ + postgresql-server-dev-${{ matrix.pg }} + + echo "" + echo "----- pg_config -----" + pg_config + echo "" + + - name: Set up PostgreSQL permissions + run: sudo chmod a+rwx `/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config --pkglibdir` `/usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config --sharedir`/extension /var/run/postgresql/ + + - name: Install cargo-pgrx + run: | + PGRX_VERSION=$(cargo metadata --format-version 1 | jq -r '.packages[]|select(.name=="pgrx")|.version') + cargo install --locked --version=$PGRX_VERSION cargo-pgrx --debug --force + cargo pgrx init --pg${{ matrix.pg }} /usr/lib/postgresql/${{ matrix.pg }}/bin/pg_config + + - name: Install required cargo + run: cargo install clippy-sarif sarif-fmt + + - name: Run rust-clippy + run: + cargo clippy + --all-features + --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt + continue-on-error: true + + - name: Upload analysis results to GitHub + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: rust-clippy-results.sarif + wait-for-processing: true \ No newline at end of file diff --git a/src/base32.rs b/src/base32.rs index 587b897..f84afb0 100644 --- a/src/base32.rs +++ b/src/base32.rs @@ -9,59 +9,57 @@ pub enum Error { } fn decode_base32_to_u128(id: &str) -> Result { - let mut id: [u8; 26] = id.as_bytes().try_into().map_err(|_| Error::InvalidData)?; - let mut max = 0; - for b in &mut id { - *b = CROCKFORD_INV[*b as usize]; - max |= *b; - } - if max > 32 || id[0] > 7 { - return Err(Error::InvalidData); - } + let mut id: [u8; 26] = id.as_bytes().try_into().map_err(|_| Error::InvalidData)?; + let mut max = 0; + for b in &mut id { + *b = CROCKFORD_INV[*b as usize]; + max |= *b; + } + if max > 32 || id[0] > 7 { + return Err(Error::InvalidData); + } - let mut out = 0u128; - for b in id { - out <<= 5; - out |= b as u128; - } + let mut out = 0u128; + for b in id { + out <<= 5; + out |= b as u128; + } - Ok(out) + Ok(out) } fn encode_u128_to_base32(data: u128) -> String { - let mut buf = [0u8; 26]; - let mut data = data; - for i in (0..26).rev() { - buf[i] = CROCKFORD[(data & 0x1f) as usize]; - debug_assert!(buf[i].is_ascii()); - data >>= 5; - } - unsafe { String::from_utf8_unchecked(buf.to_vec()) } + let mut buf = [0u8; 26]; + let mut data = data; + for i in (0..26).rev() { + buf[i] = CROCKFORD[(data & 0x1f) as usize]; + debug_assert!(buf[i].is_ascii()); + data >>= 5; + } + unsafe { String::from_utf8_unchecked(buf.to_vec()) } } const CROCKFORD: &[u8; 32] = b"0123456789abcdefghjkmnpqrstvwxyz"; const CROCKFORD_INV: &[u8; 256] = &{ - let mut output = [255; 256]; + let mut output = [255; 256]; - let mut i = 0; - while i < 32 { - output[CROCKFORD[i as usize] as usize] = i; - i += 1; - } + let mut i = 0; + while i < 32 { + output[CROCKFORD[i as usize] as usize] = i; + i += 1; + } - output + output }; - pub fn encode_base32_uuid(uuid: &Uuid) -> String { - encode_u128_to_base32(uuid.as_u128()) + encode_u128_to_base32(uuid.as_u128()) } pub fn decode_base32_uuid(encoded: &str) -> Result { - decode_base32_to_u128(encoded).map(|result: u128| Uuid::from_u128(result)) + decode_base32_to_u128(encoded).map(|result: u128| Uuid::from_u128(result)) } - #[cfg(test)] mod tests { use uuid::Uuid; @@ -76,4 +74,4 @@ mod tests { let decoded = decode_base32_uuid(&encoded).unwrap(); assert_eq!(uuid, decoded); } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 470df0b..1eea36b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,6 @@ use pgrx::prelude::*; pgrx::pg_module_magic!(); - #[pg_extern] fn typeid_generate(prefix: &str) -> TypeID { TypeID::new(TypeIDPrefix::new(prefix).unwrap(), Uuid::now_v7()) @@ -23,7 +22,10 @@ fn typeid_to_uuid(typeid: TypeID) -> pgrx::Uuid { #[pg_extern] fn uuid_to_typeid(prefix: &str, uuid: pgrx::Uuid) -> TypeID { - TypeID::new(TypeIDPrefix::new(prefix).unwrap(), Uuid::from_slice(uuid.as_bytes()).unwrap()) + TypeID::new( + TypeIDPrefix::new(prefix).unwrap(), + Uuid::from_slice(uuid.as_bytes()).unwrap(), + ) } #[pg_extern] @@ -64,8 +66,8 @@ fn typeid_ne(a: TypeID, b: TypeID) -> bool { typeid_cmp(a, b) != 0 } -extension_sql!{ - r#" +extension_sql! { +r#" CREATE OPERATOR < ( LEFTARG = typeid, RIGHTARG = typeid, @@ -114,14 +116,14 @@ extension_sql!{ OPERATOR 5 > (typeid, typeid), FUNCTION 1 typeid_cmp(typeid, typeid); "#, - name = "create_typeid_operator_class", - finalize, - } + name = "create_typeid_operator_class", + finalize, +} /// Generate a UUID v7, producing a Postgres uuid object #[pg_extern] fn uuid_generate_v7() -> pgrx::Uuid { - pgrx::Uuid::from_bytes(*Uuid::now_v7().as_bytes()) + pgrx::Uuid::from_bytes(*Uuid::now_v7().as_bytes()) } #[cfg(any(test, feature = "pg_test"))] @@ -136,7 +138,6 @@ mod tests { assert_eq!(typeid.type_prefix(), "test"); } - #[pg_test] fn test_uuid() { let uuid: pgrx::Uuid = crate::uuid_generate_v7(); @@ -146,7 +147,6 @@ mod tests { assert_eq!(converted.get_version_num(), 7); } - } /// This module is required by `cargo pgrx test` invocations. diff --git a/src/typeid.rs b/src/typeid.rs index 1055343..7540184 100644 --- a/src/typeid.rs +++ b/src/typeid.rs @@ -2,8 +2,8 @@ use core::fmt; use std::borrow::Cow; use pgrx::prelude::*; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -use serde::{Serialize, Deserialize}; use crate::base32::{decode_base32_uuid, encode_base32_uuid}; @@ -36,7 +36,7 @@ impl TypeIDPrefix { } pub fn try_unsafe(tag: &str) -> Self { - Self(tag.to_string()) + Self(tag.to_string()) } fn try_from_type_prefix(tag: &str) -> Result> { @@ -77,58 +77,62 @@ impl TypeIDPrefix { pub struct TypeID(TypeIDPrefix, Uuid); impl TypeID { - pub fn new(type_prefix: TypeIDPrefix, uuid: Uuid) -> Self { - TypeID(type_prefix, uuid) - } - - pub fn from_string(id: &str) -> Result { - // Split the input string once at the first occurrence of '_' - let (tag, id) = match id.rsplit_once('_') { - Some(("", _)) => return Err(Error::InvalidType), - Some((tag, id)) => (tag, id), - None => ("", id), - }; - - // Decode the UUID part and handle potential errors - let uuid = decode_base32_uuid(id).map_err(|_| Error::InvalidData)?; - - let prefix = TypeIDPrefix::new(tag)?; - - // Create and return the TypeID - Ok(TypeID(prefix, uuid)) - } - - pub fn type_prefix(&self) -> &str { - &self.0.to_type_prefix() - } - - pub fn uuid(&self) -> &Uuid { - &self.1 - } + pub fn new(type_prefix: TypeIDPrefix, uuid: Uuid) -> Self { + TypeID(type_prefix, uuid) + } + + pub fn from_string(id: &str) -> Result { + // Split the input string once at the first occurrence of '_' + let (tag, id) = match id.rsplit_once('_') { + Some(("", _)) => return Err(Error::InvalidType), + Some((tag, id)) => (tag, id), + None => ("", id), + }; + + // Decode the UUID part and handle potential errors + let uuid = decode_base32_uuid(id).map_err(|_| Error::InvalidData)?; + + let prefix = TypeIDPrefix::new(tag)?; + + // Create and return the TypeID + Ok(TypeID(prefix, uuid)) + } + + pub fn type_prefix(&self) -> &str { + self.0.to_type_prefix() + } + + pub fn uuid(&self) -> &Uuid { + &self.1 + } } impl fmt::Display for TypeID { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.type_prefix().is_empty() { - write!(f, "{}", encode_base32_uuid(self.uuid())) - } else { - write!(f, "{}_{}", self.type_prefix(), encode_base32_uuid(self.uuid())) - } - } + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.type_prefix().is_empty() { + write!(f, "{}", encode_base32_uuid(self.uuid())) + } else { + write!( + f, + "{}_{}", + self.type_prefix(), + encode_base32_uuid(self.uuid()) + ) + } + } } - impl InOutFuncs for TypeID { - fn input(input: &core::ffi::CStr) -> TypeID { - // Convert the input to a str and handle potential UTF-8 errors - let str_input = input.to_str().expect("text input is not valid UTF8"); - - TypeID::from_string(str_input).unwrap() - } - - fn output(&self, buffer: &mut pgrx::StringInfo) { - // Use write! macro to directly push the string representation into the buffer - use std::fmt::Write; - write!(buffer, "{}", self).expect("Failed to write to buffer"); - } -} \ No newline at end of file + fn input(input: &core::ffi::CStr) -> TypeID { + // Convert the input to a str and handle potential UTF-8 errors + let str_input = input.to_str().expect("text input is not valid UTF8"); + + TypeID::from_string(str_input).unwrap() + } + + fn output(&self, buffer: &mut pgrx::StringInfo) { + // Use write! macro to directly push the string representation into the buffer + use std::fmt::Write; + write!(buffer, "{}", self).expect("Failed to write to buffer"); + } +} diff --git a/tests/spec.rs b/tests/spec.rs index da39552..0c138df 100644 --- a/tests/spec.rs +++ b/tests/spec.rs @@ -1,6 +1,6 @@ use libtest_mimic::{Arguments, Trial}; use serde::Deserialize; -use typeid::typeid::{TypeID}; +use typeid::typeid::TypeID; use uuid::Uuid; #[derive(Deserialize)] @@ -27,8 +27,8 @@ fn main() { for test in valid { tests.push(Trial::test(format!("valid::{}", test.name), move || { let id = match TypeID::from_string(&test.typeid) { - Ok(id) => id, - Err(e) => return Err(e.to_string().into()), + Ok(id) => id, + Err(e) => return Err(e.to_string().into()), }; if id.uuid().ne(&test.uuid) { @@ -60,4 +60,4 @@ fn main() { let args = Arguments::from_args(); libtest_mimic::run(&args, tests).exit_if_failed(); -} \ No newline at end of file +}