diff --git a/.docker/prod-testing.Dockerfile b/.docker/prod-testing.Dockerfile index b0510ff6..f74d9dbc 100644 --- a/.docker/prod-testing.Dockerfile +++ b/.docker/prod-testing.Dockerfile @@ -21,7 +21,7 @@ RUN apt-get update && \ libgpgme11 \ libgnutls30 \ libuuid1 \ - libssh-gcrypt-4 \ + libssh-dev \ libhiredis1.1.0 \ libhiredis-dev \ libxml2 \ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 90547ef0..3844eb25 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,5 +2,5 @@ * @greenbone/gvm-libs-maintainers # dev ops -.github/ @greenbone/devops @greenbone/gvm-libs-maintainers -.docker/ @greenbone/devops @greenbone/gvm-libs-maintainers +.github/ @greenbone/gvm-libs-maintainers +.docker/ @greenbone/gvm-libs-maintainers diff --git a/.github/install-dependencies.sh b/.github/install-dependencies.sh index aa30d053..e398d287 100755 --- a/.github/install-dependencies.sh +++ b/.github/install-dependencies.sh @@ -13,7 +13,8 @@ apt-get update && \ libgpgme-dev \ libgnutls28-dev \ uuid-dev \ - libssh-gcrypt-dev \ + libgcrypt-dev \ + libssh-dev \ libhiredis-dev \ libxml2-dev \ libpcap-dev \ diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml deleted file mode 100644 index 749e36f1..00000000 --- a/.github/workflows/container.yml +++ /dev/null @@ -1,184 +0,0 @@ -name: Container Image Builds - -on: - push: - branches: [main, stable, oldstable] - tags: ["v*"] - pull_request: - branches: [main, stable, oldstable] - workflow_dispatch: - -jobs: - production: - name: Production Images - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: "set IS_VERSION_TAG" - run: | - echo "IS_VERSION_TAG=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}" >> $GITHUB_ENV - # set defaults - echo "IS_LATEST_TAG=false" >> $GITHUB_ENV - - name: "set IS_LATEST_TAG" - if: ( env.IS_VERSION_TAG ) - run: | - # find the latest version that is not ourself - export LATEST_VERSION=$(git tag -l | grep -v '${{ github.ref_name }}' | sort -r --version-sort) - # get major minor patch versions - IFS='.' read -r latest_major latest_minor latest_patch << EOF - $LATEST_VERSION - EOF - IFS='.' read -r tag_major tag_minor tag_patch << EOF - ${{ github.ref_name }} - EOF - # remove leading v - latest_major=$(echo $latest_major | cut -c2-) - tag_major=$(echo $tag_major | cut -c2-) - echo "$tag_major >= $latest_major" - if [[ $tag_major -ge $latest_major && ($tag_minor -ne 0 || $tag_patch -ne 0) ]]; then - # set this tag to latest and stable - echo "IS_LATEST_TAG=true" >> $GITHUB_ENV - fi - - name: "Setup meta information debian:stable" - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ github.repository }} - labels: | - org.opencontainers.image.vendor=Greenbone - org.opencontainers.image.base.name=debian:stable-slim - flavor: latest=false # no auto latest container tag for git tags - tags: | - # when IS_LATEST_TAG is set create a stable and a latest tag - type=raw,value=latest,enable=${{ env.IS_LATEST_TAG }} - type=raw,value=stable,enable=${{ env.IS_LATEST_TAG }} - # if tag version is set than create a version tags - type=semver,pattern={{version}},enable=${{ env.IS_VERSION_TAG }} - type=semver,pattern={{major}}.{{minor}},enable=${{ env.IS_VERSION_TAG }} - type=semver,pattern={{major}},enable=${{ env.IS_VERSION_TAG }} - # if we are on the main branch set edge - type=edge,branch=main - # use branch-sha otherwise for pushes to branches other then main (will not be uploaded) - type=raw,value={{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' && github.event_name == 'push' && github.ref_name != 'main' }} - # use pr-$PR_ID for pull requests (will not be uploaded) - type=ref,event=pr - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to Docker Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push Container image - uses: docker/build-push-action@v6 - with: - context: . - push: ${{ github.event_name != 'pull_request' && (github.ref_type == 'tag' || github.ref_name == 'main') }} - file: .docker/prod.Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: "Setup meta information debian:oldstable" - id: old_stable_meta - uses: docker/metadata-action@v5 - with: - images: ${{ github.repository }} - labels: | - org.opencontainers.image.vendor=Greenbone - org.opencontainers.image.base.name=debian:stable-slim - flavor: latest=false # no auto latest container tag for git tags - tags: | - # for the images provided for debian:oldstable we just provide - # oldstable on an new version or oldstable-edge when it is on main. - # oldstable-branch-sha on a branch - type=raw,value=oldstable,enable=${{ env.IS_LATEST_TAG }} - type=raw,value=oldstable-edge,enable=${{ github.ref_name == 'main' }} - type=raw,value=oldstable-{{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' && github.event_name == 'push' && github.ref_name != 'main' }} - type=ref,event=pr - - name: Build and push Container image - uses: docker/build-push-action@v6 - with: - context: . - push: ${{ github.event_name != 'pull_request' && (github.ref_type == 'tag' || github.ref_name == 'main') }} - file: .docker/prod-oldstable.Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.old_stable_meta.outputs.tags }} - labels: ${{ steps.old_stable_meta.outputs.labels }} - - - name: "Setup meta information debian:testing" - id: testing_meta - uses: docker/metadata-action@v5 - with: - images: ${{ github.repository }} - labels: | - org.opencontainers.image.vendor=Greenbone - org.opencontainers.image.base.name=debian:testing-slim - flavor: latest=false # no auto latest container tag for git tags - tags: | - # for the images provided for debian:testing we just provide - # testing on an new version or testing-edge when it is on main. - # testing-branch-sha on a branch - type=raw,value=testing,enable=${{ env.IS_LATEST_TAG }} - type=raw,value=testing-edge,enable=${{ github.ref_name == 'main' }} - type=raw,value=testing-{{branch}}-{{sha}},enable=${{ github.ref_type == 'branch' && github.event_name == 'push' && github.ref_name != 'main' }} - type=ref,event=pr - - name: Build and push Container image - uses: docker/build-push-action@v6 - with: - context: . - push: ${{ github.event_name != 'pull_request' && (github.ref_type == 'tag' || github.ref_name == 'main') }} - file: .docker/prod-testing.Dockerfile - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.testing_meta.outputs.tags }} - labels: ${{ steps.testing_meta.outputs.labels }} - - # triggers projects that work with stable branches on a new stable tag - trigger-stable-projects: - needs: production - if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') - name: Trigger update container images in related projects for new tags - strategy: - fail-fast: false - matrix: - repository: ["greenbone/gvmd", "greenbone/gsad"] - runs-on: ubuntu-latest - steps: - - name: Trigger ${{ matrix.repository }} build container image build - uses: greenbone/actions/trigger-workflow@v3 - with: - token: ${{ secrets.GREENBONE_BOT_TOKEN }} - repository: ${{ matrix.repository }} - workflow: build-container.yml - ref: main - - name: Trigger ${{ matrix.repository }} container image build - uses: greenbone/actions/trigger-workflow@v3 - with: - token: ${{ secrets.GREENBONE_BOT_TOKEN }} - repository: ${{ matrix.repository }} - workflow: container.yml - ref: main - - trigger-related-projects: - needs: production - if: github.event_name != 'pull_request' - name: Trigger update container images in related projects - strategy: - fail-fast: false - matrix: - repository: - - "greenbone/openvas-scanner" - - "greenbone/boreas" - runs-on: ubuntu-latest - steps: - - name: Trigger main ${{ matrix.repository }} container image build - uses: greenbone/actions/trigger-workflow@v3 - with: - token: ${{ secrets.GREENBONE_BOT_TOKEN }} - repository: ${{ matrix.repository }} - workflow: container.yml - ref: main diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index d9046280..0966a460 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -1,4 +1,4 @@ -name: Build and Push to Greenbone Registry +name: Build & Push to Greenbone Registry on: push: @@ -14,8 +14,8 @@ on: required: true jobs: - build: - name: Build and Push to Greenbone Registry + build-push-debian-stable-container: + name: Build and Push debian:stable to Greenbone Registry uses: greenbone/workflows/.github/workflows/container-build-push-2nd-gen.yml@main with: image-url: community/gvm-libs @@ -24,3 +24,75 @@ jobs: org.opencontainers.image.base.name=debian:stable-slim ref-name: ${{ inputs.ref-name }} secrets: inherit + + build-push-debian-oldstable-container: + name: Build and Push debian:oldstable to Greenbone Registry + uses: greenbone/workflows/.github/workflows/container-build-push-2nd-gen.yml@main + with: + build-docker-file: .docker/prod-oldstable.Dockerfile + image-url: community/gvm-libs + image-labels: | + org.opencontainers.image.vendor=Greenbone + org.opencontainers.image.base.name=debian:stable-slim + base-image-label: "oldstable" + ref-name: ${{ inputs.ref-name }} + secrets: inherit + + build-push-debian-testing-container: + name: Build and Push debian:testing to Greenbone Registry + uses: greenbone/workflows/.github/workflows/container-build-push-2nd-gen.yml@main + with: + build-docker-file: .docker/prod-testing.Dockerfile + image-url: community/gvm-libs + image-labels: | + org.opencontainers.image.vendor=Greenbone + org.opencontainers.image.base.name=debian:stable-slim + base-image-label: "testing" + ref-name: ${{ inputs.ref-name }} + secrets: inherit + + # triggers projects that work with stable branches on a new stable tag + trigger-stable-projects: + needs: build-push-debian-stable-container + if: github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + name: Trigger update container images in related projects for new tags + strategy: + fail-fast: false + matrix: + repository: ["greenbone/gvmd", "greenbone/gsad"] + runs-on: ubuntu-latest + steps: + - name: Trigger ${{ matrix.repository }} build container image build + uses: greenbone/actions/trigger-workflow@v3 + with: + token: ${{ secrets.GREENBONE_BOT_TOKEN }} + repository: ${{ matrix.repository }} + workflow: build-container.yml + ref: main + - name: Trigger ${{ matrix.repository }} container image build + uses: greenbone/actions/trigger-workflow@v3 + with: + token: ${{ secrets.GREENBONE_BOT_TOKEN }} + repository: ${{ matrix.repository }} + workflow: container.yml + ref: main + + trigger-related-projects: + needs: build-push-debian-stable-container + if: github.event_name != 'pull_request' + name: Trigger update container images in related projects + strategy: + fail-fast: false + matrix: + repository: + - "greenbone/openvas-scanner" + - "greenbone/boreas" + runs-on: ubuntu-latest + steps: + - name: Trigger main ${{ matrix.repository }} container image build + uses: greenbone/actions/trigger-workflow@v3 + with: + token: ${{ secrets.GREENBONE_BOT_TOKEN }} + repository: ${{ matrix.repository }} + workflow: ${{ matrix.repository == 'greenbone/openvas-scanner' && 'control.yml' || 'container.yml' }} + ref: main diff --git a/CMakeLists.txt b/CMakeLists.txt index e6017a5a..18091b69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ message ("-- Configuring the Greenbone Vulnerability Management Libraries...") # VERSION: Always include major, minor and patch level. project (gvm-libs - VERSION 22.11.0 + VERSION 22.12.2 LANGUAGES C) if (POLICY CMP0005) @@ -229,10 +229,9 @@ if (BUILD_TESTS AND NOT SKIP_SRC) add_custom_target (tests DEPENDS array-test alivedetection-test boreas_error-test boreas_io-test - cli-test compressutils-test cpeutils-test cvss-test ping-test - sniffer-test util-test networking-test - passwordbasedauthentication-test xmlutils-test version-test osp-test - nvti-test hosts-test) + cli-test cpeutils-test cvss-test ping-test sniffer-test util-test networking-test + passwordbasedauthentication-test xmlutils-test version-test versionutils-test + osp-test nvti-test hosts-test jsonpull-test compressutils-test) endif (BUILD_TESTS AND NOT SKIP_SRC) diff --git a/INSTALL.md b/INSTALL.md index 59c17d2e..4d8f7f91 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -49,7 +49,8 @@ Install prerequisites on Debian GNU/Linux 'Bullseye' 11: libgpgme-dev \ libgnutls28-dev \ uuid-dev \ - libssh-gcrypt-dev \ + libgcrypt-dev \ + libssh-dev \ libhiredis-dev \ libxml2-dev \ libpcap-dev \ diff --git a/util/CMakeLists.txt b/util/CMakeLists.txt index 09c7f4d5..36ba0077 100644 --- a/util/CMakeLists.txt +++ b/util/CMakeLists.txt @@ -42,6 +42,9 @@ pkg_check_modules (GPGME REQUIRED gpgme>=1.7.0) # for serverutils we need libgcrypt pkg_check_modules (GCRYPT REQUIRED libgcrypt) +# for json parsing we need cJSON +pkg_check_modules (CJSON REQUIRED libcjson>=1.7.14) + # for mqtt find_library(LIBPAHO paho-mqtt3c) message (STATUS "Looking for paho-mqtt3c ... ${LIBPAHO}") @@ -109,13 +112,13 @@ endif (BUILD_WITH_LDAP) include_directories (${GLIB_INCLUDE_DIRS} ${GPGME_INCLUDE_DIRS} ${GCRYPT_INCLUDE_DIRS} ${LIBXML2_INCLUDE_DIRS}) -set (FILES cpeutils.c passwordbasedauthentication.c compressutils.c fileutils.c gpgmeutils.c kb.c ldaputils.c - nvticache.c mqtt.c radiusutils.c serverutils.c sshutils.c uuidutils.c +set (FILES cpeutils.c passwordbasedauthentication.c compressutils.c fileutils.c gpgmeutils.c jsonpull.c kb.c + ldaputils.c nvticache.c mqtt.c radiusutils.c serverutils.c sshutils.c uuidutils.c versionutils.c xmlutils.c) -set (HEADERS cpeutils.h passwordbasedauthentication.h authutils.h compressutils.h fileutils.h gpgmeutils.h kb.h - ldaputils.h nvticache.h mqtt.h radiusutils.h serverutils.h sshutils.h - uuidutils.h xmlutils.h) +set (HEADERS cpeutils.h passwordbasedauthentication.h authutils.h compressutils.h fileutils.h gpgmeutils.h + jsonpull.h kb.h ldaputils.h nvticache.h mqtt.h radiusutils.h serverutils.h sshutils.h + uuidutils.h versionutils.h xmlutils.h) if (BUILD_STATIC) add_library (gvm_util_static STATIC ${FILES}) @@ -137,13 +140,29 @@ if (BUILD_SHARED) ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} - ${LINKER_HARDENING_FLAGS} ${CRYPT_LDFLAGS}) + ${LINKER_HARDENING_FLAGS} ${CRYPT_LDFLAGS} + ${CJSON_LDFLAGS}) endif (BUILD_SHARED) ## Tests if (BUILD_TESTS) + add_executable (jsonpull-test + EXCLUDE_FROM_ALL + jsonpull_tests.c) + + add_test (jsonpull-test jsonpull-test) + + target_include_directories (jsonpull-test PRIVATE ${CGREEN_INCLUDE_DIRS}) + + target_link_libraries (jsonpull-test ${CGREEN_LIBRARIES} + ${GLIB_LDFLAGS} ${CJSON_LDFLAGS}) + + add_custom_target (tests-jsonpull + DEPENDS jsonpull-test) + + add_executable (passwordbasedauthentication-test EXCLUDE_FROM_ALL passwordbasedauthentication_tests.c) @@ -189,14 +208,32 @@ if (BUILD_TESTS) target_link_libraries (cpeutils-test ${CGREEN_LIBRARIES} ${GLIB_LDFLAGS} ${GIO_LDFLAGS} ${GPGME_LDFLAGS} ${ZLIB_LDFLAGS} - ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} - ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} - ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} - ${LINKER_HARDENING_FLAGS}) + ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} + ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} + ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} + ${LINKER_HARDENING_FLAGS}) add_custom_target (tests-cpeutils DEPENDS cpeutils-test) + add_executable (versionutils-test + EXCLUDE_FROM_ALL + versionutils_tests.c) + + add_test (versionutils-test versionutils-test) + + target_include_directories (versionutils-test PRIVATE ${CGREEN_INCLUDE_DIRS}) + + target_link_libraries (versionutils-test ${CGREEN_LIBRARIES} + ${GLIB_LDFLAGS} ${GIO_LDFLAGS} ${GPGME_LDFLAGS} ${ZLIB_LDFLAGS} + ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} + ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} + ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} + ${LINKER_HARDENING_FLAGS}) + + add_custom_target (tests-versionutils + DEPENDS versionutils-test) + add_executable (xmlutils-test EXCLUDE_FROM_ALL xmlutils_tests.c) @@ -207,10 +244,10 @@ if (BUILD_TESTS) target_link_libraries (xmlutils-test ${CGREEN_LIBRARIES} ${GLIB_LDFLAGS} ${GIO_LDFLAGS} ${GPGME_LDFLAGS} ${ZLIB_LDFLAGS} - ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} - ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} - ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} - ${LINKER_HARDENING_FLAGS}) + ${RADIUS_LDFLAGS} ${LIBSSH_LDFLAGS} ${GNUTLS_LDFLAGS} + ${GCRYPT_LDFLAGS} ${LDAP_LDFLAGS} ${REDIS_LDFLAGS} + ${LIBXML2_LDFLAGS} ${UUID_LDFLAGS} + ${LINKER_HARDENING_FLAGS}) add_custom_target (tests-xmlutils DEPENDS xmlutils-test) diff --git a/util/cpeutils.c b/util/cpeutils.c index 3cc988a1..904ca321 100644 --- a/util/cpeutils.c +++ b/util/cpeutils.c @@ -11,6 +11,8 @@ * CPE or the CPE 2.3 formatted string binding of a CPE into a CPE struct * that corresponds to the WFN naming of a CPE. Further functions to convert * the CPE struct into the different bindings are provided. + * This file also contains a function that checks if one CPE (represented in a + * CPE struct) is a match for an other CPE (also represented in a CPE struct). */ #include "cpeutils.h" @@ -27,6 +29,69 @@ */ #define G_LOG_DOMAIN "libgvm util" +static enum set_relation +compare_component (const char *, const char *); + +static enum set_relation +compare_strings (const char *, const char *); + +static int +count_escapes (const char *, int, int); + +static gboolean +is_even_wildcards (const char *, int); + +static gboolean +has_wildcards (const char *); + +static int +index_of (const char *, const char *, int); + +static gboolean +is_string (const char *); + +static char * +get_uri_component (const char *, int); + +static char * +decode_uri_component (const char *); + +static void +unpack_sixth_uri_component (const char *, cpe_struct_t *); + +static char * +get_fs_component (const char *, int); + +static char * +unbind_fs_component (char *); + +static char * +add_quoting (const char *); + +static char * +bind_cpe_component_for_uri (const char *); + +static char * +transform_for_uri (const char *); + +static char * +pack_sixth_uri_component (const cpe_struct_t *); + +static char * +bind_cpe_component_for_fs (const char *); + +static char * +process_quoted_chars (const char *); + +static void +trim_pct (char *); + +static void +get_code (char *, const char *); + +static void +str_cpy (char **, const char *, int); + /** * @brief Convert a URI CPE to a formatted string CPE. * @@ -47,6 +112,26 @@ uri_cpe_to_fs_cpe (const char *uri_cpe) return (fs_cpe); } +/** + * @brief Convert a URI CPE to a formatted string product. + * + * @param[in] uri_cpe A CPE v2.2-conformant URI. + * + * @return A formatted string product. + */ +char * +uri_cpe_to_fs_product (const char *uri_cpe) +{ + cpe_struct_t cpe; + char *fs_cpe; + + cpe_struct_init (&cpe); + uri_cpe_to_cpe_struct (uri_cpe, &cpe); + fs_cpe = cpe_struct_to_fs_product (&cpe); + cpe_struct_free (&cpe); + return (fs_cpe); +} + /** * @brief Convert a formatted string CPE to a URI CPE. * @@ -67,6 +152,26 @@ fs_cpe_to_uri_cpe (const char *fs_cpe) return (uri_cpe); } +/** + * @brief Convert a formatted string CPE to an URI product. + * + * @param[in] fs_cpe A formatted string CPE. + * + * @return An URI product. + */ +char * +fs_cpe_to_uri_product (const char *fs_cpe) +{ + cpe_struct_t cpe; + char *uri_cpe; + + cpe_struct_init (&cpe); + fs_cpe_to_cpe_struct (fs_cpe, &cpe); + uri_cpe = cpe_struct_to_uri_product (&cpe); + cpe_struct_free (&cpe); + return (uri_cpe); +} + /** * @brief Read a URI CPE into the CPE struct. * @@ -169,6 +274,44 @@ cpe_struct_to_uri_cpe (const cpe_struct_t *cpe) return (result); } +/** + * @brief Convert a CPE struct into a URI product. + * + * @param[in] cpe A pointer to the CPE struct. + * + * @return A CPE v2.2-conformant URI product. + */ +char * +cpe_struct_to_uri_product (const cpe_struct_t *cpe) +{ + GString *uri_cpe; + char *bind_cpe_component; + uri_cpe = g_string_new ("cpe:/"); + + bind_cpe_component = bind_cpe_component_for_uri (cpe->part); + if (bind_cpe_component) + { + g_string_append_printf (uri_cpe, "%s:", bind_cpe_component); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->vendor); + if (bind_cpe_component) + { + g_string_append_printf (uri_cpe, "%s:", bind_cpe_component); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_uri (cpe->product); + if (bind_cpe_component) + { + g_string_append_printf (uri_cpe, "%s:", bind_cpe_component); + g_free (bind_cpe_component); + } + + char *result = g_string_free (uri_cpe, FALSE); + trim_pct (result); + return (result); +} + /** * @brief Read a formatted string CPE into the CPE struct. * @@ -299,6 +442,42 @@ cpe_struct_to_fs_cpe (const cpe_struct_t *cpe) return (g_string_free (fs_cpe, FALSE)); } +/** + * @brief Convert a CPE struct into a formatted string product. + * + * @param[in] cpe A pointer to the CPE struct. + * + * @return A formatted string product. + */ +char * +cpe_struct_to_fs_product (const cpe_struct_t *cpe) +{ + GString *fs_cpe; + char *bind_cpe_component; + + fs_cpe = g_string_new ("cpe:2.3:"); + + bind_cpe_component = bind_cpe_component_for_fs (cpe->part); + if (bind_cpe_component) + { + g_string_append_printf (fs_cpe, "%s:", bind_cpe_component); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->vendor); + if (bind_cpe_component) + { + g_string_append_printf (fs_cpe, "%s:", bind_cpe_component); + g_free (bind_cpe_component); + } + bind_cpe_component = bind_cpe_component_for_fs (cpe->product); + if (bind_cpe_component) + { + g_string_append_printf (fs_cpe, "%s:", bind_cpe_component); + g_free (bind_cpe_component); + } + return (g_string_free (fs_cpe, FALSE)); +} + /** * @brief Get the indexth component of a URI CPE. * diff --git a/util/cpeutils.h b/util/cpeutils.h index 2aabcb43..53297a55 100644 --- a/util/cpeutils.h +++ b/util/cpeutils.h @@ -37,53 +37,32 @@ typedef struct char * uri_cpe_to_fs_cpe (const char *); +char * +uri_cpe_to_fs_product (const char *); + char * fs_cpe_to_uri_cpe (const char *); +char * +fs_cpe_to_uri_product (const char *); + void uri_cpe_to_cpe_struct (const char *, cpe_struct_t *); char * cpe_struct_to_uri_cpe (const cpe_struct_t *); +char * +cpe_struct_to_uri_product (const cpe_struct_t *); + void fs_cpe_to_cpe_struct (const char *, cpe_struct_t *); char * cpe_struct_to_fs_cpe (const cpe_struct_t *); -static char * -get_uri_component (const char *, int); - -static char * -decode_uri_component (const char *); - -static void -unpack_sixth_uri_component (const char *, cpe_struct_t *); - -static char * -get_fs_component (const char *, int); - -static char * -unbind_fs_component (char *); - -static char * -add_quoting (const char *); - -static char * -bind_cpe_component_for_uri (const char *); - -static char * -transform_for_uri (const char *); - -static char * -pack_sixth_uri_component (const cpe_struct_t *); - -static char * -bind_cpe_component_for_fs (const char *); - -static char * -process_quoted_chars (const char *); +char * +cpe_struct_to_fs_product (const cpe_struct_t *); void cpe_struct_init (cpe_struct_t *); @@ -91,14 +70,8 @@ cpe_struct_init (cpe_struct_t *); void cpe_struct_free (cpe_struct_t *); -static void -trim_pct (char *); - -static void -get_code (char *, const char *); - -static void -str_cpy (char **, const char *, int); +gboolean +cpe_struct_match (cpe_struct_t source, cpe_struct_t target); enum set_relation { @@ -109,27 +82,4 @@ enum set_relation UNDEFINED }; -gboolean -cpe_struct_match (cpe_struct_t source, cpe_struct_t target); - -static enum set_relation -compare_component (const char *, const char *); - -static enum set_relation -compare_strings (const char *, const char *); - -static int -count_escapes (const char *, int, int); - -static gboolean -is_even_wildcards (const char *, int); - -static gboolean -has_wildcards (const char *); - -static int -index_of (const char *, const char *, int); - -static gboolean -is_string (const char *); #endif diff --git a/util/jsonpull.c b/util/jsonpull.c new file mode 100644 index 00000000..03dddf42 --- /dev/null +++ b/util/jsonpull.c @@ -0,0 +1,978 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "jsonpull.h" + +#include + +#define GVM_JSON_CHAR_EOF -1 ///< End of file +#define GVM_JSON_CHAR_ERROR -2 ///< Error reading file +#define GVM_JSON_CHAR_UNDEFINED -3 ///< Undefined state + +/** + * @brief Escapes a string according to the JSON or JSONPath standard + * + * @param[in] string The string to escape + * @param[in] single_quote Whether to escape single quotes + * + * @return The escaped string + */ +gchar * +gvm_json_string_escape (const char *string, gboolean single_quote) +{ + gchar *point; + if (string == NULL) + return NULL; + + GString *escaped = g_string_sized_new (strlen (string)); + for (point = (char *) string; *point != 0; point++) + { + unsigned char character = *point; + + if ((character > 31) && (character != '\\') + && (single_quote ? (character != '\'') : (character != '\"'))) + { + g_string_append_c (escaped, character); + } + else + { + g_string_append_c (escaped, '\\'); + switch (*point) + { + case '\\': + case '\'': + case '\"': + g_string_append_c (escaped, *point); + break; + case '\b': + g_string_append_c (escaped, 'b'); + break; + case '\f': + g_string_append_c (escaped, 'f'); + break; + case '\n': + g_string_append_c (escaped, 'n'); + break; + case '\r': + g_string_append_c (escaped, 'r'); + break; + case '\t': + g_string_append_c (escaped, 't'); + break; + default: + g_string_append_printf (escaped, "u%04x", character); + } + } + } + return g_string_free (escaped, FALSE); +} + +/** + * @brief Creates a new JSON path element. + * + * @param[in] parent_type Type of the parent (array, object, none/root) + * @param[in] depth The depth in the document tree + * + * @return The newly allocated path element + */ +gvm_json_path_elem_t * +gvm_json_pull_path_elem_new (gvm_json_pull_container_type_t parent_type, + int depth) +{ + gvm_json_path_elem_t *new_elem = g_malloc0 (sizeof (gvm_json_path_elem_t)); + new_elem->parent_type = parent_type; + new_elem->depth = depth; + return new_elem; +} + +/** + * @brief Frees a JSON path element. + * + * @param[in] elem The element to free + */ +void +gvm_json_pull_path_elem_free (gvm_json_path_elem_t *elem) +{ + g_free (elem->key); + g_free (elem); +} + +/** + * @brief Initializes a JSON pull event data structure. + * + * @param[in] event The event structure to initialize + */ +void +gvm_json_pull_event_init (gvm_json_pull_event_t *event) +{ + memset (event, 0, sizeof (gvm_json_pull_event_t)); +} + +/** + * @brief Resets a JSON pull event data structure for reuse. + * + * @param[in] event The event structure to reset + */ +void +gvm_json_pull_event_reset (gvm_json_pull_event_t *event) +{ + cJSON_free (event->value); + if (event->error_message) + g_free (event->error_message); + memset (event, 0, sizeof (gvm_json_pull_event_t)); +} + +/** + * @brief Frees all data of JSON pull event data structure. + * + * @param[in] event The event structure to clean up + */ +void +gvm_json_pull_event_cleanup (gvm_json_pull_event_t *event) +{ + cJSON_free (event->value); + if (event->error_message) + g_free (event->error_message); + memset (event, 0, sizeof (gvm_json_pull_event_t)); +} + +/** + * @brief Initializes a JSON pull parser. + * + * @param[in] parser The parser data structure to initialize + * @param[in] input_stream The JSON input stream + * @param[in] parse_buffer_limit Maximum buffer size for parsing values + * @param[in] read_buffer_size Buffer size for reading from the stream + */ +void +gvm_json_pull_parser_init_full (gvm_json_pull_parser_t *parser, + FILE *input_stream, size_t parse_buffer_limit, + size_t read_buffer_size) +{ + assert (parser); + assert (input_stream); + memset (parser, 0, sizeof (gvm_json_pull_parser_t)); + + if (parse_buffer_limit <= 0) + parse_buffer_limit = GVM_JSON_PULL_PARSE_BUFFER_LIMIT; + + if (read_buffer_size <= 0) + read_buffer_size = GVM_JSON_PULL_READ_BUFFER_SIZE; + + parser->input_stream = input_stream; + parser->path = g_queue_new (); + parser->expect = GVM_JSON_PULL_EXPECT_VALUE; + parser->parse_buffer_limit = parse_buffer_limit; + parser->parse_buffer = g_string_new (""); + parser->read_buffer_size = read_buffer_size; + parser->read_buffer = g_malloc0 (read_buffer_size); + parser->last_read_char = GVM_JSON_CHAR_UNDEFINED; +} + +/** + * @brief Initializes a JSON pull parser with default buffer sizes. + * + * @param[in] parser The parser data structure to initialize + * @param[in] input_stream The JSON input stream + */ +void +gvm_json_pull_parser_init (gvm_json_pull_parser_t *parser, FILE *input_stream) +{ + gvm_json_pull_parser_init_full (parser, input_stream, 0, 0); +} + +/** + * @brief Frees the data of a JSON pull parser. + * + * @param[in] parser The parser data structure to free the data of + */ +void +gvm_json_pull_parser_cleanup (gvm_json_pull_parser_t *parser) +{ + assert (parser); + g_queue_free_full (parser->path, + (GDestroyNotify) gvm_json_pull_path_elem_free); + g_string_free (parser->parse_buffer, TRUE); + g_free (parser->read_buffer); + memset (parser, 0, sizeof (gvm_json_pull_parser_t)); +} + +/** + * @brief Generates message for an error that occurred reading the JSON stream. + * + * @return The newly allocated error message + */ +static gchar * +gvm_json_read_stream_error_str () +{ + return g_strdup_printf ("error reading JSON stream: %s", strerror (errno)); +} + +/** + * @brief Checks if the parse buffer limit of a JSON pull parser is reached. + * + * @param[in] value_type The value type to include in the error message + * @param[in] parser The parser to check the parse buffer of + * @param[in] event Event data for error status and message if needed + * + * @return 0 if buffer size is okay, 1 if limit was reached + */ +static int +gvm_json_pull_check_parse_buffer_size (const char *value_type, + gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + if (parser->parse_buffer->len >= parser->parse_buffer_limit) + { + event->error_message = + g_strdup_printf ("%s exceeds size limit of %zu bytes", value_type, + parser->parse_buffer_limit); + event->type = GVM_JSON_PULL_EVENT_ERROR; + return 1; + } + return 0; +} + +/** + * @brief Reads the next character in a pull parser input stream. + * + * @param[in] parser The parser to read the next character from + * + * @return The character code, GVM_JSON_CHAR_ERROR or GVM_JSON_CHAR_EOF. + */ +static int +gvm_json_pull_parser_next_char (gvm_json_pull_parser_t *parser) +{ + parser->read_pos++; + if (parser->read_pos < parser->last_read_size) + { + parser->last_read_char = + (unsigned char) parser->read_buffer[parser->read_pos]; + return parser->last_read_char; + } + else + { + parser->read_pos = 0; + parser->last_read_size = fread ( + parser->read_buffer, 1, parser->read_buffer_size, parser->input_stream); + if (ferror (parser->input_stream)) + parser->last_read_char = GVM_JSON_CHAR_ERROR; + else if (parser->last_read_size <= 0) + parser->last_read_char = GVM_JSON_CHAR_EOF; + else + parser->last_read_char = + (unsigned char) parser->read_buffer[parser->read_pos]; + return parser->last_read_char; + } +} + +/** + * @brief Tries to parse the buffer content of a JSON pull parser. + * + * @param[in] parser The parser to use the parse buffer of + * @param[in] event Event set error of if necessary + * @param[in] value_name Name of the value for error message if needed + * @param[in] validate_func Function for validating the parsed value + * @param[out] cjson_value Return of the parsed cJSON object on success + * + * @return 0 success, 1 error + */ +static int +gvm_json_pull_parse_buffered (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, + const char *value_name, + cJSON_bool (*validate_func) (const cJSON *const), + cJSON **cjson_value) +{ + cJSON *parsed_value = cJSON_Parse (parser->parse_buffer->str); + *cjson_value = NULL; + if (validate_func (parsed_value) == 0) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup_printf ("error parsing %s", value_name); + cJSON_free (parsed_value); + return 1; + } + *cjson_value = parsed_value; + return 0; +} + +/** + * @brief Handles error or EOF after reading a character in JSON pull parser. + * + * @param[in] parser Parser to get the last read character from + * @param[in] event Event data to set EOF or error status in + * @param[in] allow_eof Whether to allow EOF, generate error on EOF if FALSE + */ +static void +gvm_json_pull_handle_read_end (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, gboolean allow_eof) +{ + if (parser->last_read_char == GVM_JSON_CHAR_ERROR) + { + event->error_message = gvm_json_read_stream_error_str (); + event->type = GVM_JSON_PULL_EVENT_ERROR; + } + else if (allow_eof) + event->type = GVM_JSON_PULL_EVENT_EOF; + else + { + event->error_message = g_strdup ("unexpected EOF"); + event->type = GVM_JSON_PULL_EVENT_ERROR; + } +} + +/** + * @brief Skips whitespaces in the input stream of a JSON pull parser + * + * The parser will be at the first non-whitespace character on success. + * + * @param[in] parser Parser to skip the whitespaces in + * @param[in] event Event data to set EOF or error status in + * @param[in] allow_eof Whether to allow EOF, generate error on EOF if FALSE + * + * @return 1 if EOF was reached or an error occurred, 0 otherwise + */ +static int +gvm_json_pull_skip_space (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, gboolean allow_eof) +{ + while (g_ascii_isspace (parser->last_read_char)) + gvm_json_pull_parser_next_char (parser); + if (parser->last_read_char < 0) + { + gvm_json_pull_handle_read_end (parser, event, allow_eof); + return 1; + } + return 0; +} + +/** + * @brief Parses a string in a JSON pull parser. + * + * The parser is expected to be at the opening quote mark and will be at the + * character after the closing quote mark on success. + * + * @param[in] parser Parser to handle the string value in + * @param[in] event Event data to set EOF or error status in + * @param[out] cjson_value The cJSON value for the string on success + * + * @return 1 if an error occurred (including EOF), 0 otherwise + */ +static int +gvm_json_pull_parse_string (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, cJSON **cjson_value) +{ + gboolean escape_next_char = FALSE; + g_string_truncate (parser->parse_buffer, 0); + g_string_append_c (parser->parse_buffer, '"'); + while (gvm_json_pull_parser_next_char (parser) >= 0) + { + if (gvm_json_pull_check_parse_buffer_size ("string", parser, event)) + return 1; + g_string_append_c (parser->parse_buffer, parser->last_read_char); + if (escape_next_char) + escape_next_char = FALSE; + else if (parser->last_read_char == '\\') + escape_next_char = TRUE; + else if (parser->last_read_char == '"') + break; + } + + if (parser->last_read_char < 0) + { + gvm_json_pull_handle_read_end (parser, event, FALSE); + return 1; + } + + gvm_json_pull_parser_next_char (parser); + + return gvm_json_pull_parse_buffered (parser, event, "string", cJSON_IsString, + cjson_value); +} + +/** + * @brief Parses a number in a JSON pull parser. + * + * The parser is expected to be at the first character of the number and will + * be at the first non-number character on success. + * + * @param[in] parser Parser to handle the number value in + * @param[in] event Event data to set EOF or error status in + * @param[out] cjson_value The cJSON value for the number on success. + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_number (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, cJSON **cjson_value) +{ + g_string_truncate (parser->parse_buffer, 0); + g_string_append_c (parser->parse_buffer, parser->last_read_char); + while (gvm_json_pull_parser_next_char (parser) >= 0) + { + if (gvm_json_pull_check_parse_buffer_size ("number", parser, event)) + return 1; + if (g_ascii_isdigit (parser->last_read_char) + || parser->last_read_char == '.' || parser->last_read_char == 'e' + || parser->last_read_char == '-' || parser->last_read_char == '+') + g_string_append_c (parser->parse_buffer, parser->last_read_char); + else + break; + } + + if (parser->last_read_char == GVM_JSON_CHAR_ERROR) + { + event->error_message = gvm_json_read_stream_error_str (); + event->type = GVM_JSON_PULL_EVENT_ERROR; + return 1; + } + + return gvm_json_pull_parse_buffered (parser, event, "number", cJSON_IsNumber, + cjson_value); +} + +/** + * @brief Parses a keyword value in a JSON pull parser. + * + * The parser is expected to be at the first character of the keyword and will + * be at the first character after the keyword on success. + * + * @param[in] parser Parser to handle the keyword value in + * @param[in] event Event data to set EOF or error status in + * @param[in] keyword The expected keyword, e.g. "null", "true", "false". + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_keyword (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event, const char *keyword) +{ + for (size_t i = 0; i < strlen (keyword); i++) + { + if (parser->last_read_char < 0) + { + gvm_json_pull_handle_read_end (parser, event, FALSE); + return 1; + } + else if (parser->last_read_char != keyword[i]) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = + g_strdup_printf ("misspelled keyword '%s'", keyword); + return 1; + } + gvm_json_pull_parser_next_char (parser); + } + return 0; +} + +/** + * @brief Updates the expectation for a JSON pull parser according to the path. + * + * @param[in] parser The parser to update. + */ +static void +parse_value_next_expect (gvm_json_pull_parser_t *parser) +{ + if (parser->path->length) + parser->expect = GVM_JSON_PULL_EXPECT_COMMA; + else + parser->expect = GVM_JSON_PULL_EXPECT_EOF; +} + +/** + * @brief Handles the case that an object key is expected in a JSON pull parser. + * + * This will continue the parsing until the value is expected, the end of the + * current object was reached or an error occurred. + * + * @param[in] parser Parser to process + * @param[in] event Event data to set error or end of object status in + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_key (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + if (gvm_json_pull_skip_space (parser, event, FALSE)) + return 1; + + cJSON *key_cjson = NULL; + gchar *key_str; + gvm_json_path_elem_t *path_elem; + + switch (parser->last_read_char) + { + case '"': + if (gvm_json_pull_parse_string (parser, event, &key_cjson)) + return 1; + key_str = g_strdup (key_cjson->valuestring); + cJSON_free (key_cjson); + + // Expect colon: + if (gvm_json_pull_skip_space (parser, event, FALSE)) + { + g_free (key_str); + return 1; + } + if (parser->last_read_char != ':') + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup_printf ("expected colon"); + g_free (key_str); + return 1; + } + gvm_json_pull_parser_next_char (parser); + + path_elem = g_queue_peek_tail (parser->path); + g_free (path_elem->key); + path_elem->key = key_str; + parser->expect = GVM_JSON_PULL_EXPECT_VALUE; + + break; + case '}': + event->type = GVM_JSON_PULL_EVENT_OBJECT_END; + event->value = NULL; + gvm_json_pull_path_elem_free (g_queue_pop_tail (parser->path)); + parse_value_next_expect (parser); + gvm_json_pull_parser_next_char (parser); + break; + case ']': + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing square bracket"); + return 1; + default: + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected character"); + return 1; + } + + return 0; +} + +/** + * @brief Handles the case that a comma is expected in a JSON pull parser. + * + * This will continue the parsing until a comma or the end of the + * current array/object was reached or an error occurred. + * + * @param[in] parser Parser to process + * @param[in] event Event data to set error or end of object status in + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_comma (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + if (gvm_json_pull_skip_space (parser, event, FALSE)) + return 1; + + gvm_json_path_elem_t *path_elem = NULL; + switch (parser->last_read_char) + { + case ',': + path_elem = g_queue_peek_tail (parser->path); + path_elem->index++; + if (path_elem->parent_type == GVM_JSON_PULL_CONTAINER_OBJECT) + parser->expect = GVM_JSON_PULL_EXPECT_KEY; + else + parser->expect = GVM_JSON_PULL_EXPECT_VALUE; + gvm_json_pull_parser_next_char (parser); + break; + case ']': + path_elem = g_queue_peek_tail (parser->path); + if (path_elem == NULL + || path_elem->parent_type != GVM_JSON_PULL_CONTAINER_ARRAY) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing square bracket"); + return 1; + } + event->type = GVM_JSON_PULL_EVENT_ARRAY_END; + event->value = NULL; + gvm_json_pull_path_elem_free (g_queue_pop_tail (parser->path)); + parse_value_next_expect (parser); + gvm_json_pull_parser_next_char (parser); + break; + case '}': + path_elem = g_queue_peek_tail (parser->path); + if (path_elem == NULL + || path_elem->parent_type != GVM_JSON_PULL_CONTAINER_OBJECT) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing curly brace"); + return 1; + } + event->type = GVM_JSON_PULL_EVENT_OBJECT_END; + event->value = NULL; + gvm_json_pull_path_elem_free (g_queue_pop_tail (parser->path)); + parse_value_next_expect (parser); + gvm_json_pull_parser_next_char (parser); + break; + default: + event->error_message = g_strdup ("expected comma or end of container"); + event->type = GVM_JSON_PULL_EVENT_ERROR; + return 1; + } + return 0; +} + +/** + * @brief Handles the case that a value is expected in a JSON pull parser. + * + * This will continue the parsing until a value or the end of the + * current array/object was parsed or an error occurred. + * + * @param[in] parser Parser to process + * @param[in] event Event data to set error or end of object status in + * + * @return 1 if an error occurred, 0 otherwise + */ +static int +gvm_json_pull_parse_value (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + if (gvm_json_pull_skip_space (parser, event, FALSE)) + return 1; + + cJSON *cjson_value = NULL; + gvm_json_path_elem_t *path_elem = NULL; + + switch (parser->last_read_char) + { + case '"': + if (gvm_json_pull_parse_string (parser, event, &cjson_value)) + return 1; + event->type = GVM_JSON_PULL_EVENT_STRING; + event->value = cjson_value; + parse_value_next_expect (parser); + break; + case 'n': + if (gvm_json_pull_parse_keyword (parser, event, "null")) + return 1; + event->type = GVM_JSON_PULL_EVENT_NULL; + event->value = cJSON_CreateNull (); + parse_value_next_expect (parser); + break; + case 'f': + if (gvm_json_pull_parse_keyword (parser, event, "false")) + return 1; + event->type = GVM_JSON_PULL_EVENT_BOOLEAN; + event->value = cJSON_CreateFalse (); + parse_value_next_expect (parser); + break; + case 't': + if (gvm_json_pull_parse_keyword (parser, event, "true")) + return 1; + event->type = GVM_JSON_PULL_EVENT_BOOLEAN; + event->value = cJSON_CreateTrue (); + parse_value_next_expect (parser); + break; + case '[': + event->type = GVM_JSON_PULL_EVENT_ARRAY_START; + event->value = NULL; + parser->path_add = gvm_json_pull_path_elem_new ( + GVM_JSON_PULL_CONTAINER_ARRAY, parser->path->length); + parser->expect = GVM_JSON_PULL_EXPECT_VALUE; + gvm_json_pull_parser_next_char (parser); + break; + case ']': + path_elem = g_queue_peek_tail (parser->path); + if (path_elem == NULL + || path_elem->parent_type != GVM_JSON_PULL_CONTAINER_ARRAY) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing square bracket"); + return 1; + } + event->type = GVM_JSON_PULL_EVENT_ARRAY_END; + event->value = NULL; + gvm_json_pull_path_elem_free (g_queue_pop_tail (parser->path)); + parse_value_next_expect (parser); + gvm_json_pull_parser_next_char (parser); + break; + case '{': + event->type = GVM_JSON_PULL_EVENT_OBJECT_START; + event->value = NULL; + parser->path_add = gvm_json_pull_path_elem_new ( + GVM_JSON_PULL_CONTAINER_OBJECT, parser->path->length); + parser->expect = GVM_JSON_PULL_EXPECT_KEY; + gvm_json_pull_parser_next_char (parser); + break; + case '}': + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected closing curly brace"); + return 1; + break; + default: + if (g_ascii_isdigit (parser->last_read_char) + || parser->last_read_char == '-') + { + if (gvm_json_pull_parse_number (parser, event, &cjson_value)) + return 1; + event->type = GVM_JSON_PULL_EVENT_NUMBER; + event->value = cjson_value; + parse_value_next_expect (parser); + } + else + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup ("unexpected character"); + return 1; + } + } + return 0; +} + +/** + * @brief Get the next event from a JSON pull parser. + * + * Note: This invalidates previous event data like the cJSON value. + * + * @param[in] parser The JSON pull parser to process until the next event + * @param[in] event Structure to store event data in. + */ +void +gvm_json_pull_parser_next (gvm_json_pull_parser_t *parser, + gvm_json_pull_event_t *event) +{ + assert (parser); + assert (event); + + gvm_json_pull_event_reset (event); + if (parser->last_read_char == GVM_JSON_CHAR_UNDEFINED) + { + // Handle first read of the stream + if (gvm_json_pull_parser_next_char (parser) < 0) + { + gvm_json_pull_handle_read_end (parser, event, TRUE); + return; + } + } + event->path = parser->path; + + // Delayed addition to path after a container start element + if (parser->path_add) + { + g_queue_push_tail (parser->path, parser->path_add); + parser->path_add = NULL; + } + + // Check for expected end of file + if (parser->expect == GVM_JSON_PULL_EXPECT_EOF) + { + gvm_json_pull_skip_space (parser, event, TRUE); + + if (parser->last_read_char == GVM_JSON_CHAR_ERROR) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = gvm_json_read_stream_error_str (); + } + else if (parser->last_read_char != GVM_JSON_CHAR_EOF) + { + event->type = GVM_JSON_PULL_EVENT_ERROR; + event->error_message = g_strdup_printf ( + "unexpected character at end of file (%d)", parser->last_read_char); + return; + } + return; + } + + if (parser->expect == GVM_JSON_PULL_EXPECT_COMMA) + { + if (gvm_json_pull_parse_comma (parser, event)) + return; + } + + if (parser->expect == GVM_JSON_PULL_EXPECT_KEY) + { + if (gvm_json_pull_parse_key (parser, event)) + return; + } + + if (parser->expect == GVM_JSON_PULL_EXPECT_VALUE) + { + gvm_json_pull_parse_value (parser, event); + } +} + +/** + * @brief Expands the current array or object of a JSON pull parser. + * + * This should be called after an array or object start event. + * + * @param[in] parser Parser to get the current container element from + * @param[out] error_message Error message output + * + * @return The expanded container as a cJSON object if successful, else NULL + */ +cJSON * +gvm_json_pull_expand_container (gvm_json_pull_parser_t *parser, + gchar **error_message) +{ + gvm_json_path_elem_t *path_tail = NULL; + + int start_depth; + gboolean in_string, escape_next_char, in_expanded_container; + cJSON *expanded; + + g_string_truncate (parser->parse_buffer, 0); + + if (error_message) + *error_message = NULL; + + // require "path_add" to only allow expansion at start of container + if (parser->path_add) + { + path_tail = parser->path_add; + g_queue_push_tail (parser->path, path_tail); + parser->path_add = NULL; + } + + if (path_tail && path_tail->parent_type == GVM_JSON_PULL_CONTAINER_ARRAY) + g_string_append_c (parser->parse_buffer, '['); + else if (path_tail + && path_tail->parent_type == GVM_JSON_PULL_CONTAINER_OBJECT) + g_string_append_c (parser->parse_buffer, '{'); + else + { + if (error_message) + *error_message = + g_strdup ("can only expand after array or object start"); + return NULL; + } + + start_depth = path_tail->depth; + in_string = escape_next_char = FALSE; + in_expanded_container = TRUE; + + while (parser->last_read_char >= 0 && in_expanded_container) + { + if (parser->parse_buffer->len >= parser->parse_buffer_limit) + { + if (error_message) + *error_message = + g_strdup_printf ("container exceeds size limit of %zu bytes", + parser->parse_buffer_limit); + return NULL; + } + + g_string_append_c (parser->parse_buffer, parser->last_read_char); + + if (escape_next_char) + { + escape_next_char = FALSE; + } + else if (in_string) + { + escape_next_char = (parser->last_read_char == '\\'); + in_string = (parser->last_read_char != '"'); + } + else + { + switch (parser->last_read_char) + { + case '"': + in_string = TRUE; + break; + case '[': + path_tail = gvm_json_pull_path_elem_new ( + GVM_JSON_PULL_CONTAINER_ARRAY, parser->path->length); + g_queue_push_tail (parser->path, path_tail); + break; + case '{': + path_tail = gvm_json_pull_path_elem_new ( + GVM_JSON_PULL_CONTAINER_OBJECT, parser->path->length); + g_queue_push_tail (parser->path, path_tail); + break; + case ']': + path_tail = g_queue_pop_tail (parser->path); + if (path_tail->parent_type != GVM_JSON_PULL_CONTAINER_ARRAY) + { + if (error_message) + *error_message = + g_strdup ("unexpected closing square bracket"); + return NULL; + } + if (path_tail->depth == start_depth) + in_expanded_container = FALSE; + break; + case '}': + path_tail = g_queue_pop_tail (parser->path); + if (path_tail->parent_type != GVM_JSON_PULL_CONTAINER_OBJECT) + { + if (error_message) + *error_message = + g_strdup ("unexpected closing curly brace"); + return NULL; + } + if (path_tail->depth == start_depth) + in_expanded_container = FALSE; + break; + } + } + gvm_json_pull_parser_next_char (parser); + } + + if (parser->last_read_char == GVM_JSON_CHAR_ERROR) + { + if (error_message) + *error_message = gvm_json_read_stream_error_str (); + return NULL; + } + else if (in_expanded_container && parser->last_read_char == GVM_JSON_CHAR_EOF) + { + if (error_message) + *error_message = g_strdup ("unexpected EOF"); + return NULL; + } + + expanded = cJSON_Parse (parser->parse_buffer->str); + g_string_truncate (parser->parse_buffer, 0); + parse_value_next_expect (parser); + + if (expanded == NULL && error_message) + *error_message = g_strdup ("could not parse expanded container"); + + return expanded; +} + +/** + * @brief Appends a string path element to a JSONPath string. + * + * @param[in] path_elem The path element to append + * @param[in] path_string The path string to append to + */ +static void +gvm_json_path_string_add_elem (gvm_json_path_elem_t *path_elem, + GString *path_string) +{ + if (path_elem->parent_type == GVM_JSON_PULL_CONTAINER_OBJECT) + { + gchar *escaped_key = gvm_json_string_escape (path_elem->key, TRUE); + g_string_append_printf (path_string, "['%s']", escaped_key); + g_free (escaped_key); + } + else + g_string_append_printf (path_string, "[%d]", path_elem->index); +} + +/** + * @brief Converts a path as used by a JSON pull parser to a JSONPath string. + * + * @param[in] path The path to convert + * + * @return Newly allocated string of the path in JSONPath bracket notation + */ +gchar * +gvm_json_path_to_string (GQueue *path) +{ + GString *path_string = g_string_new ("$"); + g_queue_foreach (path, (GFunc) gvm_json_path_string_add_elem, path_string); + return g_string_free (path_string, FALSE); +} diff --git a/util/jsonpull.h b/util/jsonpull.h new file mode 100644 index 00000000..0b057a49 --- /dev/null +++ b/util/jsonpull.h @@ -0,0 +1,137 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#ifndef _GVM_JSONPULL_H +#define _GVM_JSONPULL_H + +#define _GNU_SOURCE + +#include +#include +#include + +/** + * @brief Type of container the parser is currently in + */ +typedef enum +{ + GVM_JSON_PULL_CONTAINER_NONE = 0, ///< No container / document root + GVM_JSON_PULL_CONTAINER_ARRAY, ///< Array + GVM_JSON_PULL_CONTAINER_OBJECT, ///< Object +} gvm_json_pull_container_type_t; + +/** + * @brief Path element types for the JSON pull parser. + */ +typedef struct gvm_json_path_elem +{ + gvm_json_pull_container_type_t parent_type; ///< parent container type + int index; ///< Index of the element within the parent + char *key; ///< Key if element is in an object + int depth; ///< Number of ancestor elements +} gvm_json_path_elem_t; + +/** + * @brief Event types for the JSON pull parser + */ +typedef enum +{ + GVM_JSON_PULL_EVENT_UNDEFINED = 0, + GVM_JSON_PULL_EVENT_ARRAY_START, + GVM_JSON_PULL_EVENT_ARRAY_END, + GVM_JSON_PULL_EVENT_OBJECT_START, + GVM_JSON_PULL_EVENT_OBJECT_END, + GVM_JSON_PULL_EVENT_STRING, + GVM_JSON_PULL_EVENT_NUMBER, + GVM_JSON_PULL_EVENT_BOOLEAN, + GVM_JSON_PULL_EVENT_NULL, + GVM_JSON_PULL_EVENT_EOF, + GVM_JSON_PULL_EVENT_ERROR, +} gvm_json_pull_event_type_t; + +/** + * @brief Event generated by the JSON pull parser. + */ +typedef struct +{ + gvm_json_pull_event_type_t type; ///< Type of event + GQueue *path; ///< Path to the event value + cJSON *value; ///< Value for non-container value events + gchar *error_message; ///< Error message, NULL on success +} gvm_json_pull_event_t; + +/** + * @brief Expected token state for the JSON pull parser + */ +typedef enum +{ + GVM_JSON_PULL_EXPECT_UNDEFINED = 0, ///< Undefined state + GVM_JSON_PULL_EXPECT_VALUE, ///< Expect start of a value + GVM_JSON_PULL_EXPECT_KEY, ///< Expect start of a key + GVM_JSON_PULL_EXPECT_COMMA, ///< Expect comma or container end brace + GVM_JSON_PULL_EXPECT_EOF ///< Expect end of file +} gvm_json_pull_expect_t; + +#define GVM_JSON_PULL_PARSE_BUFFER_LIMIT 10485760 + +#define GVM_JSON_PULL_READ_BUFFER_SIZE 4096 + +/** + * @brief A json pull parser + */ +typedef struct +{ + GQueue *path; ///< Path to the current value + gvm_json_path_elem_t *path_add; ///< Path elem to add in next step + gvm_json_pull_expect_t expect; ///< Current expected token + int keyword_pos; ///< Position in a keyword like "true" or "null" + FILE *input_stream; ///< Input stream + char *read_buffer; ///< Stream reading buffer + size_t read_buffer_size; ///< Size of the stream reading buffer + size_t last_read_size; ///< Size of last stream read + int last_read_char; ///< Character last read from stream + size_t read_pos; ///< Position in current read + GString *parse_buffer; ///< Buffer for parsing values and object keys + size_t parse_buffer_limit; ///< Maximum parse buffer size +} gvm_json_pull_parser_t; + +gchar * +gvm_json_string_escape (const char *, gboolean); + +gvm_json_path_elem_t * +gvm_json_pull_path_elem_new (gvm_json_pull_container_type_t, int); + +void +gvm_json_pull_path_elem_free (gvm_json_path_elem_t *); + +void +gvm_json_pull_event_init (gvm_json_pull_event_t *); + +void +gvm_json_pull_event_reset (gvm_json_pull_event_t *); + +void +gvm_json_pull_event_cleanup (gvm_json_pull_event_t *); + +void +gvm_json_pull_parser_init_full (gvm_json_pull_parser_t *, FILE *, size_t, + size_t); + +void +gvm_json_pull_parser_init (gvm_json_pull_parser_t *, FILE *); + +void +gvm_json_pull_parser_cleanup (gvm_json_pull_parser_t *); + +void +gvm_json_pull_parser_next (gvm_json_pull_parser_t *, gvm_json_pull_event_t *); + +cJSON * +gvm_json_pull_expand_container (gvm_json_pull_parser_t *, gchar **); + +gchar * +gvm_json_path_to_string (GQueue *path); + +#endif /* _GVM_JSONPULL_H */ diff --git a/util/jsonpull_tests.c b/util/jsonpull_tests.c new file mode 100644 index 00000000..444f5a66 --- /dev/null +++ b/util/jsonpull_tests.c @@ -0,0 +1,1225 @@ +/* SPDX-FileCopyrightText: 2019-2023 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "jsonpull.c" + +#include +#include +#include + +Describe (jsonpull); +BeforeEach (jsonpull) +{ +} +AfterEach (jsonpull) +{ +} + +/* + * Helper function to open a string as a read-only stream. + */ +static inline FILE * +fstropen_r (const char *str) +{ + return fmemopen ((void *) str, strlen (str), "r"); +} + +static ssize_t +read_with_error_on_eof (void *stream_cookie, char *buf, size_t size) +{ + FILE *stream = stream_cookie; + ssize_t ret = fread (buf, 1, size, stream); + if (ret <= 0) + { + errno = EIO; + return -1; + } + else + return ret; +} + +#define INIT_JSON_PARSER(json_string) \ + gvm_json_pull_event_t event; \ + gvm_json_pull_parser_t parser; \ + FILE *jsonstream; \ + jsonstream = fstropen_r (json_string); \ + gvm_json_pull_event_init (&event); \ + gvm_json_pull_parser_init_full (&parser, jsonstream, 100, 4); + +#define INIT_READ_ERROR_JSON_PARSER(json_string) \ + gvm_json_pull_event_t event; \ + gvm_json_pull_parser_t parser; \ + FILE *jsonstream = fstropen_r (json_string); \ + cookie_io_functions_t io_functions = {.read = read_with_error_on_eof, \ + .write = NULL, \ + .seek = NULL, \ + .close = NULL}; \ + FILE *errorstream = fopencookie (jsonstream, "r", io_functions); \ + gvm_json_pull_event_init (&event); \ + gvm_json_pull_parser_init_full (&parser, errorstream, 100, 4); + +#define CLEANUP_JSON_PARSER \ + gvm_json_pull_event_cleanup (&event); \ + gvm_json_pull_parser_cleanup (&parser); \ + fclose (jsonstream); + +#define CHECK_PATH_EQUALS(expected_path_str) \ + path_str = gvm_json_path_to_string (event.path); \ + assert_that (path_str, is_equal_to_string (expected_path_str)); \ + g_free (path_str); + +#define JSON_READ_ERROR "error reading JSON stream: Input/output error" + +Ensure (jsonpull, can_json_escape_strings) +{ + const char *unescaped_string = "\"'Abc\\\b\f\n\r\t\001Äöü'\""; + const char *escaped_string_dq = "\\\"'Abc\\\\\\b\\f\\n\\r\\t\\u0001Äöü'\\\""; + const char *escaped_string_sq = "\"\\'Abc\\\\\\b\\f\\n\\r\\t\\u0001Äöü\\'\""; + + gchar *escaped_string = NULL; + escaped_string = gvm_json_string_escape (NULL, FALSE); + assert_that (escaped_string, is_null); + + escaped_string = gvm_json_string_escape (unescaped_string, FALSE); + assert_that (escaped_string, is_equal_to_string (escaped_string_dq)); + g_free (escaped_string); + + escaped_string = gvm_json_string_escape (unescaped_string, TRUE); + assert_that (escaped_string, is_equal_to_string (escaped_string_sq)); + g_free (escaped_string); +} + +Ensure (jsonpull, can_init_parser_with_defaults) +{ + gvm_json_pull_parser_t parser; + FILE *strfile = fstropen_r ("[]"); + + gvm_json_pull_parser_init (&parser, strfile); + assert_that (parser.input_stream, is_equal_to (strfile)); + assert_that (parser.parse_buffer_limit, + is_equal_to (GVM_JSON_PULL_PARSE_BUFFER_LIMIT)); + assert_that (parser.read_buffer_size, + is_equal_to (GVM_JSON_PULL_READ_BUFFER_SIZE)); +} + +Ensure (jsonpull, can_parse_false) +{ + INIT_JSON_PARSER ("false"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_BOOLEAN)); + assert_that (cJSON_IsBool (event.value), is_true); + assert_that (cJSON_IsFalse (event.value), is_true); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_true) +{ + INIT_JSON_PARSER ("true"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_BOOLEAN)); + assert_that (cJSON_IsBool (event.value), is_true); + assert_that (cJSON_IsTrue (event.value), is_true); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_null) +{ + INIT_JSON_PARSER ("null"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NULL)); + assert_that (cJSON_IsNull (event.value), is_true); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_empty_strings) +{ + INIT_JSON_PARSER ("\"\""); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("")); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_strings_with_content) +{ + INIT_JSON_PARSER ("\n\"123\\tXYZ\\nÄöü\"\n"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("123\tXYZ\nÄöü")); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_integer_numbers) +{ + INIT_JSON_PARSER ("-0987"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (-987)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_floating_point_numbers) +{ + INIT_JSON_PARSER ("\t\n 1.2345e+4\n"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valuedouble, is_equal_to (1.2345e+4)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_empty_arrays) +{ + INIT_JSON_PARSER ("[ ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_single_elem_arrays) +{ + gchar *path_str; + INIT_JSON_PARSER ("[ 123 ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (123)); + CHECK_PATH_EQUALS ("$[0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_multiple_elem_arrays) +{ + gchar *path_str; + INIT_JSON_PARSER ("[123, \"ABC\", null]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (123)); + CHECK_PATH_EQUALS ("$[0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("ABC")); + CHECK_PATH_EQUALS ("$[1]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NULL)); + CHECK_PATH_EQUALS ("$[2]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_empty_objects) +{ + INIT_JSON_PARSER ("{ }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_single_elem_objects) +{ + gchar *path_str; + INIT_JSON_PARSER ("{ \"keyA\": \"valueA\" }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("valueA")); + CHECK_PATH_EQUALS ("$['keyA']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_multiple_elem_objects) +{ + gchar *path_str; + INIT_JSON_PARSER ("{ \"keyA\": \"valueA\", \"keyB\":12345 }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("valueA")); + CHECK_PATH_EQUALS ("$['keyA']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (12345)); + CHECK_PATH_EQUALS ("$['keyB']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_parse_nested_containers) +{ + gchar *path_str; + INIT_JSON_PARSER ("[{\"A\":null, \"B\":{\"C\": [1,2]}, \"D\":\"3\"}, [4]]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$[0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NULL)); + CHECK_PATH_EQUALS ("$[0]['A']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$[0]['B']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[0]['B']['C']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (1)); + CHECK_PATH_EQUALS ("$[0]['B']['C'][0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (2)); + CHECK_PATH_EQUALS ("$[0]['B']['C'][1]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + CHECK_PATH_EQUALS ("$[0]['B']['C']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + CHECK_PATH_EQUALS ("$[0]['B']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_STRING)); + assert_that (event.value->valuestring, is_equal_to_string ("3")); + CHECK_PATH_EQUALS ("$[0]['D']"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + CHECK_PATH_EQUALS ("$[0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[1]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + assert_that (event.value->valueint, is_equal_to (4)); + CHECK_PATH_EQUALS ("$[1][0]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + CHECK_PATH_EQUALS ("$[1]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_expand_arrays) +{ + gchar *path_str, *error_message; + cJSON *expanded, *child; + INIT_JSON_PARSER ("[[], [1], [2, [3]], [\"A\", \"\\\"B]\"]]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$"); + + // empty array + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[0]"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_equal_to_string (NULL)); + assert_that (expanded, is_not_null); + assert_that (cJSON_IsArray (expanded), is_true); + assert_that (expanded->child, is_null); + cJSON_free (expanded); + + // single-element array + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[1]"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (expanded, is_not_null); + assert_that (cJSON_IsArray (expanded), is_true); + child = expanded->child; + assert_that (child, is_not_null); + assert_that (cJSON_IsNumber (child), is_true); + assert_that (child->valueint, is_equal_to (1)); + child = child->next; + assert_that (child, is_null); + cJSON_free (expanded); + + // multi-element array + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[2]"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (expanded, is_not_null); + assert_that (cJSON_IsArray (expanded), is_true); + child = expanded->child; + assert_that (child, is_not_null); + assert_that (cJSON_IsNumber (child), is_true); + assert_that (child->valueint, is_equal_to (2)); + child = child->next; + assert_that (child, is_not_null); + assert_that (cJSON_IsArray (child), is_true); + assert_that (child->child->valueint, is_equal_to (3)); + cJSON_free (expanded); + + // string array + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + CHECK_PATH_EQUALS ("$[3]"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (expanded, is_not_null); + assert_that (cJSON_IsArray (expanded), is_true); + child = expanded->child; + assert_that (child, is_not_null); + assert_that (cJSON_IsString (child), is_true); + assert_that (child->valuestring, is_equal_to_string ("A")); + child = child->next; + assert_that (child, is_not_null); + assert_that (cJSON_IsString (child), is_true); + assert_that (child->valuestring, is_equal_to_string ("\"B]")); + cJSON_free (expanded); + + // array end and EOF + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_END)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, can_expand_objects) +{ + gchar *path_str, *error_message; + cJSON *expanded, *child; + INIT_JSON_PARSER ( + "{\"A\":{}, \"B\": {\"C\": \"\\\"D}\", \"E\":123, \"F\":{}}}"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$['A']"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (cJSON_IsObject (expanded), is_true); + assert_that (expanded->child, is_null); + cJSON_free (expanded); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + CHECK_PATH_EQUALS ("$['B']"); + expanded = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (error_message, is_null); + assert_that (cJSON_IsObject (expanded), is_true); + child = expanded->child; + assert_that (child, is_not_null); + assert_that (cJSON_IsString (child), is_true); + assert_that (child->string, is_equal_to_string ("C")); + assert_that (child->valuestring, is_equal_to_string ("\"D}")); + child = child->next; + assert_that (child, is_not_null); + assert_that (cJSON_IsNumber (child), is_true); + assert_that (child->string, is_equal_to_string ("E")); + assert_that (child->valueint, is_equal_to (123)); + child = child->next; + assert_that (child, is_not_null); + assert_that (cJSON_IsObject (child), is_true); + assert_that (child->string, is_equal_to_string ("F")); + assert_that (child->child, is_null); + cJSON_free (expanded); + + // object end and EOF + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_END)); + CHECK_PATH_EQUALS ("$"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_EOF)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("123"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + gvm_json_pull_parser_cleanup (&parser); + gvm_json_pull_event_cleanup (&event); + fclose (jsonstream); +} + +Ensure (jsonpull, fails_for_misspelled_true) +{ + INIT_JSON_PARSER ("trxyz"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("misspelled keyword 'true'")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_incomplete_true) +{ + INIT_JSON_PARSER ("tru"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_misspelled_false) +{ + INIT_JSON_PARSER ("falxyz"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("misspelled keyword 'false'")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_misspelled_null) +{ + INIT_JSON_PARSER ("nulx"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("misspelled keyword 'null'")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_string_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("\"ABCDEFG\""); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_string_eof) +{ + INIT_JSON_PARSER ("\"no closing quote here"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_overlong_string) +{ + INIT_JSON_PARSER ("\"This should be too long for a small parse buffer\""); + parser.parse_buffer_limit = 10; + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("string exceeds size limit of 10 bytes")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_string) +{ + INIT_JSON_PARSER ("\"This has an invalid escape sequence: \\x\""); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("error parsing string")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_number_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("12345.123456789"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_overlong_number) +{ + INIT_READ_ERROR_JSON_PARSER ("12345.123456789"); + parser.parse_buffer_limit = 10; + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("number exceeds size limit of 10 bytes")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_number) +{ + INIT_JSON_PARSER ("-+e"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("error parsing number")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_array_eof) +{ + INIT_JSON_PARSER ("["); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_array_eof_after_value) +{ + INIT_JSON_PARSER ("[123"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_array_eof_after_comma) +{ + INIT_JSON_PARSER ("[123,"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_array_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("[ "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_array_bracket) +{ + INIT_JSON_PARSER ("[}"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing curly brace")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_array_bracket_after_value) +{ + INIT_JSON_PARSER ("[123}"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing curly brace")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_array_other_char) +{ + INIT_JSON_PARSER ("[!"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected character")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_array_other_char_after_value) +{ + INIT_JSON_PARSER ("[123!"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("expected comma or end of container")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_key_eof) +{ + INIT_JSON_PARSER ("{"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_key_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("{ "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_key_invalid_string) +{ + INIT_JSON_PARSER ("{\"invalid escape:\\x\": 123}"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("error parsing string")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_object_key_bracket) +{ + INIT_JSON_PARSER ("{]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing square bracket")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_invalid_object_key_other_char) +{ + INIT_JSON_PARSER ("{!"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected character")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_colon_eof) +{ + INIT_JSON_PARSER ("{\"A\" "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_colon_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("{\"A\" "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_colon_other_char) +{ + INIT_JSON_PARSER ("{\"A\"!"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("expected colon")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_value_eof) +{ + INIT_JSON_PARSER ("{\"A\": "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_value_read_error) +{ + INIT_READ_ERROR_JSON_PARSER ("{\"A\": "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_value_curly_brace) +{ + INIT_JSON_PARSER ("{\"A\": }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing curly brace")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_value_square_bracket) +{ + INIT_JSON_PARSER ("{\"A\": ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing square bracket")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_eof_after_value) +{ + INIT_JSON_PARSER ("{\"A\": 123"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_square_bracket_after_value) +{ + INIT_JSON_PARSER ("{\"A\": 123 ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected closing square bracket")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_object_eof_after_comma) +{ + INIT_JSON_PARSER ("{\"A\": 123, "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string ("unexpected EOF")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_read_error_after_doc_end) +{ + INIT_READ_ERROR_JSON_PARSER ("123 "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, is_equal_to_string (JSON_READ_ERROR)); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_content_after_doc_end) +{ + INIT_JSON_PARSER ("123 456"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ERROR)); + assert_that (event.error_message, + is_equal_to_string ("unexpected character at end of file (52)")); + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_before_container) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[]"); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, is_equal_to_string ("can only expand after" + " array or object start")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_after_value) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[123, 456]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_NUMBER)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, is_equal_to_string ("can only expand after" + " array or object start")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_invalid_content) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[invalid content]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, + is_equal_to_string ("could not parse expanded container")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_overlong) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[1234567890.123456780]"); + parser.parse_buffer_limit = 10; + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, + is_equal_to_string ("container exceeds size limit of 10 bytes")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_unexpected_curly_brace) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[ 123 }"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, + is_equal_to_string ("unexpected closing curly brace")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_unexpected_square_bracket) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("{ \"A\": 123 ]"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_OBJECT_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, + is_equal_to_string ("unexpected closing square bracket")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_eof) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_JSON_PARSER ("[ 123"); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, is_equal_to_string ("unexpected EOF")); + + CLEANUP_JSON_PARSER; +} + +Ensure (jsonpull, fails_for_expand_read_error) +{ + cJSON *cjson_value; + gchar *error_message; + INIT_READ_ERROR_JSON_PARSER ("[ 123 "); + + gvm_json_pull_parser_next (&parser, &event); + assert_that (event.type, is_equal_to (GVM_JSON_PULL_EVENT_ARRAY_START)); + + cjson_value = gvm_json_pull_expand_container (&parser, &error_message); + assert_that (cjson_value, is_null); + assert_that (error_message, is_equal_to_string (JSON_READ_ERROR)); + + CLEANUP_JSON_PARSER; +} + +int +main (int argc, char **argv) +{ + TestSuite *suite; + + suite = create_test_suite (); + + add_test_with_context (suite, jsonpull, can_json_escape_strings); + + add_test_with_context (suite, jsonpull, can_init_parser_with_defaults); + + add_test_with_context (suite, jsonpull, can_parse_false); + add_test_with_context (suite, jsonpull, can_parse_true); + add_test_with_context (suite, jsonpull, can_parse_null); + + add_test_with_context (suite, jsonpull, can_parse_empty_strings); + add_test_with_context (suite, jsonpull, can_parse_strings_with_content); + + add_test_with_context (suite, jsonpull, can_parse_integer_numbers); + add_test_with_context (suite, jsonpull, can_parse_floating_point_numbers); + + add_test_with_context (suite, jsonpull, can_parse_empty_arrays); + add_test_with_context (suite, jsonpull, can_parse_single_elem_arrays); + add_test_with_context (suite, jsonpull, can_parse_multiple_elem_arrays); + + add_test_with_context (suite, jsonpull, can_parse_empty_objects); + add_test_with_context (suite, jsonpull, can_parse_single_elem_objects); + add_test_with_context (suite, jsonpull, can_parse_multiple_elem_objects); + add_test_with_context (suite, jsonpull, can_parse_nested_containers); + add_test_with_context (suite, jsonpull, can_expand_arrays); + add_test_with_context (suite, jsonpull, can_expand_objects); + + add_test_with_context (suite, jsonpull, fails_for_read_error); + + add_test_with_context (suite, jsonpull, fails_for_misspelled_true); + add_test_with_context (suite, jsonpull, fails_for_incomplete_true); + add_test_with_context (suite, jsonpull, fails_for_misspelled_false); + add_test_with_context (suite, jsonpull, fails_for_misspelled_null); + + add_test_with_context (suite, jsonpull, fails_for_string_eof); + add_test_with_context (suite, jsonpull, fails_for_string_read_error); + add_test_with_context (suite, jsonpull, fails_for_overlong_string); + add_test_with_context (suite, jsonpull, fails_for_invalid_string); + + add_test_with_context (suite, jsonpull, fails_for_number_read_error); + add_test_with_context (suite, jsonpull, fails_for_overlong_number); + add_test_with_context (suite, jsonpull, fails_for_invalid_number); + + add_test_with_context (suite, jsonpull, fails_for_array_eof); + add_test_with_context (suite, jsonpull, fails_for_array_eof_after_value); + add_test_with_context (suite, jsonpull, fails_for_array_eof_after_comma); + add_test_with_context (suite, jsonpull, fails_for_array_read_error); + add_test_with_context (suite, jsonpull, fails_for_invalid_array_bracket); + add_test_with_context (suite, jsonpull, + fails_for_invalid_array_bracket_after_value); + add_test_with_context (suite, jsonpull, fails_for_invalid_array_other_char); + add_test_with_context (suite, jsonpull, + fails_for_invalid_array_other_char_after_value); + + add_test_with_context (suite, jsonpull, fails_for_object_key_eof); + add_test_with_context (suite, jsonpull, fails_for_object_key_read_error); + add_test_with_context (suite, jsonpull, fails_for_object_key_invalid_string); + add_test_with_context (suite, jsonpull, fails_for_invalid_object_key_bracket); + add_test_with_context (suite, jsonpull, + fails_for_invalid_object_key_other_char); + + add_test_with_context (suite, jsonpull, fails_for_object_colon_eof); + add_test_with_context (suite, jsonpull, fails_for_object_colon_read_error); + add_test_with_context (suite, jsonpull, fails_for_object_colon_other_char); + + add_test_with_context (suite, jsonpull, fails_for_object_value_eof); + add_test_with_context (suite, jsonpull, fails_for_object_value_read_error); + add_test_with_context (suite, jsonpull, fails_for_object_value_curly_brace); + add_test_with_context (suite, jsonpull, + fails_for_object_value_square_bracket); + add_test_with_context (suite, jsonpull, fails_for_object_eof_after_value); + add_test_with_context (suite, jsonpull, fails_for_object_eof_after_comma); + add_test_with_context (suite, jsonpull, + fails_for_object_square_bracket_after_value); + + add_test_with_context (suite, jsonpull, fails_for_read_error_after_doc_end); + add_test_with_context (suite, jsonpull, fails_for_content_after_doc_end); + + add_test_with_context (suite, jsonpull, fails_for_expand_before_container); + add_test_with_context (suite, jsonpull, fails_for_expand_after_value); + add_test_with_context (suite, jsonpull, fails_for_expand_invalid_content); + add_test_with_context (suite, jsonpull, fails_for_expand_overlong); + add_test_with_context (suite, jsonpull, + fails_for_expand_unexpected_curly_brace); + add_test_with_context (suite, jsonpull, + fails_for_expand_unexpected_square_bracket); + add_test_with_context (suite, jsonpull, fails_for_expand_read_error); + add_test_with_context (suite, jsonpull, fails_for_expand_eof); + + if (argc > 1) + return run_single_test (suite, argv[1], create_text_reporter ()); + return run_test_suite (suite, create_text_reporter ()); +} diff --git a/util/versionutils.c b/util/versionutils.c new file mode 100644 index 00000000..d0a2eb60 --- /dev/null +++ b/util/versionutils.c @@ -0,0 +1,363 @@ +/* SPDX-FileCopyrightText: 2009-2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Functions to handle version numbers / version strings. + * + * Up to now this library provides a function to compare two version numbers / + * two version strings to decide which version is the newer one. + */ + +#include "versionutils.h" + +#include +#include +#include +#include +#include + +#undef G_LOG_DOMAIN +/** + * @brief GLib logging domain. + */ +#define G_LOG_DOMAIN "libgvm util" + +static gchar * +prepare_version_string (const char *); + +static int +get_release_state (const char *, int); + +static char * +get_part (const char *, int); + +static gboolean +is_text (const char *); + +static char * +str_cpy (char *, int); + +/** + * @brief Compare two version strings representing a software version + * to decide which version is newer. + * + * @param[in] version1 The first version string to compare. + * @param[in] version2 The second version string to compare. + * + * @return Returns a value > 0 if version1 is newer than version2. + * Returns 0 if version1 is the same than version2. + * Returns a value between -1 and -4 if version2 is newer + * than version1. + * Returns -5 if the result is undefined. + */ +int +cmp_versions (const char *version1, const char *version2) +{ + char *ver1, *ver2; + char *part1, *part2; + int index1 = 0, index2 = 0; + int release_state1 = 0, release_state2 = 0; + int rs1, rs2; + + ver1 = prepare_version_string (version1); + ver2 = prepare_version_string (version2); + + if (ver1 == NULL || ver2 == NULL) + { + g_free (ver1); + g_free (ver2); + return (-5); + } + if (strcmp (ver1, ver2) == 0) + { + g_free (ver1); + g_free (ver2); + return (0); + } + + if ((release_state1 = get_release_state (ver1, index1))) + index1++; + if ((release_state2 = get_release_state (ver2, index2))) + index2++; + + part1 = get_part (ver1, index1); + part2 = get_part (ver2, index2); + while (part1 && part2) + { + if (strcmp (part1, part2) == 0) + { + index1++; + index2++; + g_free (part1); + g_free (part2); + part1 = get_part (ver1, index1); + part2 = get_part (ver2, index2); + continue; + } + else + break; + } + + if (part1 == NULL && part2 == NULL) + return (release_state2 - release_state1); + + if (is_text (part1) || is_text (part2)) + { + if (part1) + g_free (part1); + if (part2) + g_free (part2); + return (-5); // undefined + } + + rs1 = get_release_state (ver1, index1); + rs2 = get_release_state (ver2, index2); + + if ((rs1 && release_state1) || (rs2 && release_state2)) + return (-5); // undefined + + if (part1 == NULL) + { + g_free (part2); + if (rs2) + return (rs2 - release_state1); + else + return (-1); + } + + if (part2 == NULL) + { + g_free (part1); + if (rs1) + return (release_state2 - rs1); + else + return (1); + } + + int ret = -5; + + if (rs1 && rs2) + ret = rs2 - rs1; + + if (rs1) + ret = -1; + + if (rs2) + ret = 1; + + if (!rs1 && !rs2 && atoi (part1) < atoi (part2)) + ret = -1; + + if (!rs1 && !rs2 && atoi (part1) == atoi (part2)) + ret = 0; + + if (!rs1 && !rs2 && atoi (part1) > atoi (part2)) + ret = 1; + + g_free (part1); + g_free (part2); + g_free (ver1); + g_free (ver2); + return (ret); +} + +/** + * @brief Prepare the version string for comparison. + * + * @param[in] version The version string to generate the prepared + * version string from. + * + * @return Returns a prepared copy of the version string version. + */ +static gchar * +prepare_version_string (const char *version) +{ + char prep_version[2048]; + char *ver; + int index_v, index_pv; + gboolean is_digit; + + if (!version) + return (NULL); + + if (strlen (version) > 1024) + return (NULL); + + ver = g_strdup (version); + + /* set all characters to lowercase */ + char *c = ver; + for (; *c; c++) + *c = tolower (*c); + + index_v = index_pv = 0; + + is_digit = g_ascii_isdigit (ver[0]); + + while (index_v < (int) strlen (ver) && index_pv < 2047) + { + if (ver[index_v] == '\\') + { + index_v++; + continue; + } + + if (ver[index_v] == '_' || ver[index_v] == '-' || ver[index_v] == '+' + || ver[index_v] == ':' || ver[index_v] == '.') + { + if (index_pv > 0 && prep_version[index_pv - 1] != '.') + { + prep_version[index_pv] = '.'; + index_pv++; + } + index_v++; + continue; + } + + if (is_digit != g_ascii_isdigit (ver[index_v])) + { + is_digit = !is_digit; + if (index_pv > 0 && prep_version[index_pv - 1] != '.') + { + prep_version[index_pv] = '.'; + index_pv++; + } + } + + if (ver[index_v] == 'r') + { + if (strstr (ver + index_v, "releasecandidate") == ver + index_v) + { + prep_version[index_pv] = 'r'; + prep_version[index_pv + 1] = 'c'; + index_pv += 2; + index_v += 16; + continue; + } + if ((strstr (ver + index_v, "release-candidate") == ver + index_v) + || (strstr (ver + index_v, "release_candidate") == ver + index_v)) + { + prep_version[index_pv] = 'r'; + prep_version[index_pv + 1] = 'c'; + index_pv += 2; + index_v += 17; + continue; + } + } + + prep_version[index_pv] = ver[index_v]; + index_v++; + index_pv++; + } + + prep_version[index_pv] = '\0'; + g_free (ver); + return (g_strdup (prep_version)); +} + +/** + * @brief Gets the release state of a specified part of the version string + * if any. + * + * @param[in] version The version string to get the release state from. + * @param[in] index The part of the version string to check. + * + * @return Returns 0 if there is no release state, returns 4 if the release + * state is "development" (dev), returns 3 if the state is "alpha", + * 2 if the state is beta and 1 if the state is release candidate (rc). + */ +static int +get_release_state (const char *version, int index) +{ + char *part; + int rel_stat = 0; + + part = get_part (version, index); + + if (part == NULL) + return (0); + + if (strcmp (part, "dev") == 0 || strcmp (part, "development") == 0) + rel_stat = 4; + if (strcmp (part, "alpha") == 0) + rel_stat = 3; + if (strcmp (part, "beta") == 0) + rel_stat = 2; + if (strcmp (part, "rc") == 0) + rel_stat = 1; + + g_free (part); + return (rel_stat); +} + +/** + * @brief Gets the part of the version string that is specified by index. + * + * @param[in] version The version string to get the part from. + * @param[in] index The part of the version string to return. + * + * @return Returns a copy of the specified part of the version string. + */ +static char * +get_part (const char *version, int index) +{ + int dot_count = 0; + int begin, end; + + for (begin = 0; begin < (int) strlen (version) && dot_count < index; begin++) + { + if (version[begin] == '.') + dot_count++; + } + + if (begin == (int) strlen (version)) + return (NULL); + + for (end = begin + 1; end < (int) strlen (version) && version[end] != '.'; + end++) + ; + + return (str_cpy ((char *) (version + begin), end - begin)); +} + +/** + * @brief Checks if a given part of the version string is plain text. + * + * @param[in] part The part of the version string to check. + * + * @return Returns TRUE if part contains only plain text, FALSE otherwise. + */ +static gboolean +is_text (const char *part) +{ + if (!part) + return (FALSE); + if (strcmp (part, "dev") == 0 || strcmp (part, "alpha") == 0 + || strcmp (part, "beta") == 0 || strcmp (part, "rc") == 0) + return (FALSE); + if (g_ascii_isdigit (*part)) + return (FALSE); + return (TRUE); +} + +/** + * @brief Copy size characters of a string to an newly allocated new string. + * + * @param[in] src The string the first size characters are to be copied + * from. + * @param[in] size The number of characters to copy. + * + * @return The copy of the first size characters of src as a new string. + */ +static char * +str_cpy (char *source, int size) +{ + char *result; + result = (char *) g_malloc (size + 1); + memset (result, 0, size + 1); + strncpy (result, source, size); + return (result); +} diff --git a/util/versionutils.h b/util/versionutils.h new file mode 100644 index 00000000..d093189c --- /dev/null +++ b/util/versionutils.h @@ -0,0 +1,20 @@ +/* SPDX-FileCopyrightText: 2009-2024 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +/** + * @file + * @brief Headers for version utils. + */ + +#ifndef _GVM_VERSIONUTILS_H +#define _GVM_VERSIONUTILS_H + +#include +#include + +int +cmp_versions (const char *, const char *); + +#endif diff --git a/util/versionutils_tests.c b/util/versionutils_tests.c new file mode 100644 index 00000000..c7e0502b --- /dev/null +++ b/util/versionutils_tests.c @@ -0,0 +1,88 @@ +/* SPDX-FileCopyrightText: 2019-2023 Greenbone AG + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "versionutils.c" + +#include +#include + +Describe (versionutils); +BeforeEach (versionutils) +{ +} + +AfterEach (versionutils) +{ +} + +/* parse_entity */ + +Ensure (versionutils, cmp_versions) +{ + char *version1, *version2; + int result; + + version1 = "test"; + version2 = "test-1"; + result = cmp_versions (version1, version2); + assert_that (result, is_less_than (0)); + assert_that (result, is_greater_than (-5)); + + version1 = "beta-test-2"; + version2 = "test_1"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); + + version1 = "beta-test-2"; + version2 = "test-2.beta"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (0)); + + version1 = "test-2.beta"; + version2 = "test-2.a"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-5)); + + version1 = "test-2.beta"; + version2 = "test-2.1"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-1)); + + version1 = "test-2.release_candidate"; + version2 = "test-2"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-1)); + + version1 = "test-2.release_candidate2"; + version2 = "test-2.release_candidate1"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); + + version1 = "test-2.release_candidatea"; + version2 = "test-2.release_candidateb"; + result = cmp_versions (version1, version2); + assert_that (result, is_equal_to (-5)); + + version1 = "2024-06-24"; + version2 = "2024-06-23"; + result = cmp_versions (version1, version2); + assert_that (result, is_greater_than (0)); +} + +/* Test suite. */ +int +main (int argc, char **argv) +{ + TestSuite *suite; + + suite = create_test_suite (); + + add_test_with_context (suite, versionutils, cmp_versions); + + if (argc > 1) + return run_single_test (suite, argv[1], create_text_reporter ()); + + return run_test_suite (suite, create_text_reporter ()); +}