diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4e3d919a..22172982 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -44,6 +44,10 @@ jobs: find $(pwd) -name '.git' -exec bash -c 'git config --global --add safe.directory ${0%/.git}' {} \; ./autogen.sh --enable-embedded-yajl ./configure --enable-embedded-yajl CFLAGS='-Wall -Wextra -Werror' + # Run make check first to catch test failures with accessible logs + make -j $(nproc) + make check || (cat test-suite.log 2>/dev/null; exit 1) + # Now run distcheck for distribution validation make -j $(nproc) distcheck DISTCHECK_CONFIGURE_FLAGS="--enable-embedded-yajl" # check that the working dir is clean git describe --broken --dirty --all | grep -qv dirty diff --git a/.gitignore b/.gitignore index 50a39501..9d50e155 100644 --- a/.gitignore +++ b/.gitignore @@ -66,7 +66,8 @@ src/ocispec/validate tests/.deps/ tests/.dirstamp -tests/test-[0-9]* +tests/test-[0-9] +tests/test-[0-9][0-9] tests/*.log tests/*.trs test-suite.log diff --git a/Makefile.am b/Makefile.am index a64205d3..3ba58375 100644 --- a/Makefile.am +++ b/Makefile.am @@ -210,6 +210,12 @@ tests_test_10_LDADD = $(TESTS_LDADD) tests_test_11_SOURCES = tests/test-11.c tests_test_11_LDADD = $(TESTS_LDADD) +tests_test_12_SOURCES = tests/test-12.c +tests_test_12_LDADD = $(TESTS_LDADD) + +tests_test_13_SOURCES = tests/test-13.c +tests_test_13_LDADD = $(TESTS_LDADD) + src_ocispec_validate_SOURCES = src/ocispec/validate.c src_ocispec_validate_LDADD = $(TESTS_LDADD) @@ -223,7 +229,9 @@ TESTS = tests/test-1 \ tests/test-8 \ tests/test-9 \ tests/test-10 \ - tests/test-11 + tests/test-11 \ + tests/test-12 \ + tests/test-13 noinst_PROGRAMS = src/ocispec/validate $(TESTS) diff --git a/src/ocispec/sources.py b/src/ocispec/sources.py index ff82184d..bd066e3d 100755 --- a/src/ocispec/sources.py +++ b/src/ocispec/sources.py @@ -1164,9 +1164,12 @@ def emit_clone(self, c_file, obj, prefix, indent=1): # Clone function doesn't use json_ prefix clone_name = self.map_name.replace('json_', '', 1) emit(c_file, f''' - ret->{obj.fixname} = clone_{clone_name} (src->{obj.fixname}); - if (ret->{obj.fixname} == NULL) - return NULL; + if (src->{obj.fixname} != NULL) + {{ + ret->{obj.fixname} = clone_{clone_name} (src->{obj.fixname}); + if (ret->{obj.fixname} == NULL) + return NULL; + }} ''', indent=indent) @@ -1984,9 +1987,11 @@ def make_clone(obj, c_file, prefix): {typename} * clone_{typename} ({typename} *src) {{ - (void) src; /* Silence compiler warning. */ __auto_cleanup(free_{typename}) {typename} *ret = NULL; + if (src == NULL) + return NULL; + ret = calloc (1, sizeof (*ret)); if (ret == NULL) return NULL; diff --git a/tests/test-12.c b/tests/test-12.c new file mode 100644 index 00000000..32f68085 --- /dev/null +++ b/tests/test-12.c @@ -0,0 +1,355 @@ +/* Copyright (C) 2025 Giuseppe Scrivano + +libocispec is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +libocispec is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libocispec. If not, see . + +*/ + +/* Test clone round-trip: parse -> clone -> generate -> parse -> compare */ + +#include "config.h" +#include +#include +#include +#include +#include "ocispec/runtime_spec_schema_config_schema.h" + +static int +test_clone_roundtrip (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *original = NULL; + runtime_spec_schema_config_schema *cloned = NULL; + runtime_spec_schema_config_schema *reparsed = NULL; + char *json_from_original = NULL; + char *json_from_clone = NULL; + int ret = 0; + + /* Parse original */ + original = runtime_spec_schema_config_schema_parse_file ("tests/data/config.json", 0, &err); + if (original == NULL) + { + printf ("parse error: %s\n", err); + ret = 1; + goto out; + } + + /* Clone it */ + cloned = clone_runtime_spec_schema_config_schema (original); + if (cloned == NULL) + { + printf ("clone failed (original hostname=%s, ociVersion=%s)\n", + original->hostname ? original->hostname : "(null)", + original->oci_version ? original->oci_version : "(null)"); + ret = 2; + goto out; + } + + /* Generate JSON from both */ + json_from_original = runtime_spec_schema_config_schema_generate_json (original, 0, &err); + if (json_from_original == NULL) + { + printf ("generate from original error: %s\n", err); + ret = 3; + goto out; + } + + json_from_clone = runtime_spec_schema_config_schema_generate_json (cloned, 0, &err); + if (json_from_clone == NULL) + { + printf ("generate from clone error: %s\n", err); + ret = 4; + goto out; + } + + /* The generated JSON should be identical */ + if (strcmp (json_from_original, json_from_clone) != 0) + { + printf ("JSON mismatch between original and clone\n"); + printf ("Original:\n%s\n", json_from_original); + printf ("Clone:\n%s\n", json_from_clone); + ret = 5; + goto out; + } + + /* Verify we can parse the cloned output */ + reparsed = runtime_spec_schema_config_schema_parse_data (json_from_clone, 0, &err); + if (reparsed == NULL) + { + printf ("reparse error: %s\n", err); + ret = 6; + goto out; + } + + /* Verify key fields match */ + if (strcmp (original->hostname, cloned->hostname) != 0) + { + printf ("hostname mismatch\n"); + ret = 7; + goto out; + } + + if (strcmp (original->process->cwd, cloned->process->cwd) != 0) + { + printf ("cwd mismatch\n"); + ret = 8; + goto out; + } + + if (original->process->user->uid != cloned->process->user->uid) + { + printf ("uid mismatch\n"); + ret = 9; + goto out; + } + + if (original->linux->namespaces_len != cloned->linux->namespaces_len) + { + printf ("namespaces_len mismatch\n"); + ret = 10; + goto out; + } + + printf ("clone roundtrip test passed\n"); + +out: + free (err); + free (json_from_original); + free (json_from_clone); + free_runtime_spec_schema_config_schema (original); + free_runtime_spec_schema_config_schema (cloned); + free_runtime_spec_schema_config_schema (reparsed); + return ret; +} + +static int +test_modify_clone_independence (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *original = NULL; + runtime_spec_schema_config_schema *cloned = NULL; + int ret = 0; + const char *original_hostname; + + original = runtime_spec_schema_config_schema_parse_file ("tests/data/config.json", 0, &err); + if (original == NULL) + { + printf ("parse error: %s\n", err); + ret = 1; + goto out; + } + + original_hostname = original->hostname; + + cloned = clone_runtime_spec_schema_config_schema (original); + if (cloned == NULL) + { + printf ("clone failed\n"); + ret = 2; + goto out; + } + + /* Modify the clone */ + free (cloned->hostname); + cloned->hostname = strdup ("modified-hostname"); + if (cloned->hostname == NULL) + { + printf ("strdup failed\n"); + ret = 3; + goto out; + } + + /* Original should be unchanged */ + if (strcmp (original->hostname, original_hostname) != 0) + { + printf ("original was modified when clone was changed\n"); + ret = 4; + goto out; + } + + if (strcmp (cloned->hostname, "modified-hostname") != 0) + { + printf ("clone modification failed\n"); + ret = 5; + goto out; + } + + printf ("clone independence test passed\n"); + +out: + free (err); + free_runtime_spec_schema_config_schema (original); + free_runtime_spec_schema_config_schema (cloned); + return ret; +} + +static int +test_present_flags (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *config = NULL; + int ret = 0; + + config = runtime_spec_schema_config_schema_parse_file ("tests/data/config.json", 0, &err); + if (config == NULL) + { + printf ("parse error: %s\n", err); + ret = 1; + goto out; + } + + /* Check that present flags are set correctly */ + if (!config->process->terminal_present) + { + printf ("terminal_present should be true\n"); + ret = 2; + goto out; + } + + if (!config->process->user->uid_present) + { + printf ("uid_present should be true\n"); + ret = 3; + goto out; + } + + /* gid is not set in the test data */ + if (config->process->user->gid_present) + { + printf ("gid_present should be false\n"); + ret = 4; + goto out; + } + + /* Verify numeric values */ + if (config->process->user->uid != 101) + { + printf ("uid should be 101, got %u\n", config->process->user->uid); + ret = 5; + goto out; + } + + printf ("present flags test passed\n"); + +out: + free (err); + free_runtime_spec_schema_config_schema (config); + return ret; +} + +static int +test_array_cloning (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *original = NULL; + runtime_spec_schema_config_schema *cloned = NULL; + int ret = 0; + size_t i; + + original = runtime_spec_schema_config_schema_parse_file ("tests/data/config.json", 0, &err); + if (original == NULL) + { + printf ("parse error: %s\n", err); + ret = 1; + goto out; + } + + cloned = clone_runtime_spec_schema_config_schema (original); + if (cloned == NULL) + { + printf ("clone failed\n"); + ret = 2; + goto out; + } + + /* Verify array lengths match */ + if (original->process->args_len != cloned->process->args_len) + { + printf ("args_len mismatch\n"); + ret = 3; + goto out; + } + + /* Verify array contents match */ + for (i = 0; i < original->process->args_len; i++) + { + if (strcmp (original->process->args[i], cloned->process->args[i]) != 0) + { + printf ("args[%zu] mismatch\n", i); + ret = 4; + goto out; + } + } + + /* Verify mounts array */ + if (original->mounts_len != cloned->mounts_len) + { + printf ("mounts_len mismatch\n"); + ret = 5; + goto out; + } + + for (i = 0; i < original->mounts_len; i++) + { + if (strcmp (original->mounts[i]->destination, cloned->mounts[i]->destination) != 0) + { + printf ("mounts[%zu]->destination mismatch\n", i); + ret = 6; + goto out; + } + } + + printf ("array cloning test passed\n"); + +out: + free (err); + free_runtime_spec_schema_config_schema (original); + free_runtime_spec_schema_config_schema (cloned); + return ret; +} + +int +main (void) +{ + int ret; + + ret = test_clone_roundtrip (); + if (ret != 0) + { + printf ("test_clone_roundtrip failed: %d\n", ret); + return ret; + } + + ret = test_modify_clone_independence (); + if (ret != 0) + { + printf ("test_modify_clone_independence failed: %d\n", ret); + return ret; + } + + ret = test_present_flags (); + if (ret != 0) + { + printf ("test_present_flags failed: %d\n", ret); + return ret; + } + + ret = test_array_cloning (); + if (ret != 0) + { + printf ("test_array_cloning failed: %d\n", ret); + return ret; + } + + return 0; +} diff --git a/tests/test-13.c b/tests/test-13.c new file mode 100644 index 00000000..d873da20 --- /dev/null +++ b/tests/test-13.c @@ -0,0 +1,445 @@ +/* Copyright (C) 2025 Giuseppe Scrivano + +libocispec is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +libocispec is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libocispec. If not, see . + +*/ + +/* Test error handling and edge cases */ + +#include "config.h" +#include +#include +#include +#include +#include "ocispec/runtime_spec_schema_config_schema.h" +#include "ocispec/image_spec_schema_config_schema.h" + +static int +test_parse_invalid_json (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *config = NULL; + int ret = 0; + + /* Parse invalid JSON */ + config = runtime_spec_schema_config_schema_parse_data ("{invalid json", 0, &err); + if (config != NULL) + { + printf ("expected parse to fail for invalid JSON\n"); + free_runtime_spec_schema_config_schema (config); + ret = 1; + goto out; + } + + /* Error should be set */ + if (err == NULL) + { + printf ("expected error to be set\n"); + ret = 2; + goto out; + } + + printf ("parse invalid json test passed (error: %s)\n", err); + +out: + free (err); + return ret; +} + +static int +test_parse_minimal_json (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *config = NULL; + char *json_buf = NULL; + int ret = 0; + /* Minimal valid config - ociVersion is required */ + const char *minimal_json = "{\"ociVersion\": \"1.0.0\"}"; + + config = runtime_spec_schema_config_schema_parse_data (minimal_json, 0, &err); + if (config == NULL) + { + printf ("parse minimal object failed: %s\n", err); + ret = 1; + goto out; + } + + if (config->oci_version == NULL || strcmp (config->oci_version, "1.0.0") != 0) + { + printf ("ociVersion mismatch\n"); + ret = 2; + goto out; + } + + /* Generate JSON from minimal object */ + json_buf = runtime_spec_schema_config_schema_generate_json (config, 0, &err); + if (json_buf == NULL) + { + printf ("generate from minimal object failed: %s\n", err); + ret = 3; + goto out; + } + + printf ("parse minimal json test passed\n"); + +out: + free (err); + free (json_buf); + free_runtime_spec_schema_config_schema (config); + return ret; +} + +static int +test_parse_nonexistent_file (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *config = NULL; + int ret = 0; + + config = runtime_spec_schema_config_schema_parse_file ("/nonexistent/path/config.json", 0, &err); + if (config != NULL) + { + printf ("expected parse to fail for nonexistent file\n"); + free_runtime_spec_schema_config_schema (config); + ret = 1; + goto out; + } + + if (err == NULL) + { + printf ("expected error to be set\n"); + ret = 2; + goto out; + } + + printf ("parse nonexistent file test passed (error: %s)\n", err); + +out: + free (err); + return ret; +} + +static int +test_null_input (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *config = NULL; + int ret = 0; + + /* Parse NULL data should fail gracefully */ + config = runtime_spec_schema_config_schema_parse_data (NULL, 0, &err); + if (config != NULL) + { + printf ("expected parse to fail for NULL input\n"); + free_runtime_spec_schema_config_schema (config); + ret = 1; + goto out; + } + + printf ("null input test passed\n"); + +out: + free (err); + return ret; +} + +static int +test_empty_arrays (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *config = NULL; + char *json_buf = NULL; + int ret = 0; + const char *json_with_empty_arrays = "{" + "\"ociVersion\": \"1.0.0\"," + "\"process\": {" + " \"args\": []," + " \"env\": []," + " \"cwd\": \"/\"" + "}," + "\"root\": {\"path\": \"rootfs\"}" + "}"; + + config = runtime_spec_schema_config_schema_parse_data (json_with_empty_arrays, 0, &err); + if (config == NULL) + { + printf ("parse with empty arrays failed: %s\n", err); + ret = 1; + goto out; + } + + /* Verify empty arrays are handled correctly */ + if (config->process->args_len != 0) + { + printf ("expected args_len to be 0, got %zu\n", config->process->args_len); + ret = 2; + goto out; + } + + if (config->process->env_len != 0) + { + printf ("expected env_len to be 0, got %zu\n", config->process->env_len); + ret = 3; + goto out; + } + + /* Generate JSON should work */ + json_buf = runtime_spec_schema_config_schema_generate_json (config, 0, &err); + if (json_buf == NULL) + { + printf ("generate with empty arrays failed: %s\n", err); + ret = 4; + goto out; + } + + printf ("empty arrays test passed\n"); + +out: + free (err); + free (json_buf); + free_runtime_spec_schema_config_schema (config); + return ret; +} + +static int +test_string_with_special_chars (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *config = NULL; + runtime_spec_schema_config_schema *reparsed = NULL; + char *json_buf = NULL; + int ret = 0; + const char *json_with_special = "{" + "\"ociVersion\": \"1.0.0\"," + "\"hostname\": \"host\\nwith\\nnewlines\"," + "\"process\": {" + " \"args\": [\"echo\", \"hello\\tworld\"]," + " \"cwd\": \"/path/with spaces/and\\\"quotes\\\"\"" + "}," + "\"root\": {\"path\": \"rootfs\"}" + "}"; + + config = runtime_spec_schema_config_schema_parse_data (json_with_special, 0, &err); + if (config == NULL) + { + printf ("parse with special chars failed: %s\n", err); + ret = 1; + goto out; + } + + /* Verify special characters are preserved */ + if (strstr (config->hostname, "\n") == NULL) + { + printf ("newline not preserved in hostname\n"); + ret = 2; + goto out; + } + + /* Generate and reparse to verify round-trip */ + json_buf = runtime_spec_schema_config_schema_generate_json (config, 0, &err); + if (json_buf == NULL) + { + printf ("generate with special chars failed: %s\n", err); + ret = 3; + goto out; + } + + reparsed = runtime_spec_schema_config_schema_parse_data (json_buf, 0, &err); + if (reparsed == NULL) + { + printf ("reparse with special chars failed: %s\n", err); + ret = 4; + goto out; + } + + if (strcmp (config->hostname, reparsed->hostname) != 0) + { + printf ("hostname mismatch after round-trip\n"); + ret = 5; + goto out; + } + + printf ("string with special chars test passed\n"); + +out: + free (err); + free (json_buf); + free_runtime_spec_schema_config_schema (config); + free_runtime_spec_schema_config_schema (reparsed); + return ret; +} + +static int +test_large_numbers (void) +{ + parser_error err = NULL; + runtime_spec_schema_config_schema *config = NULL; + runtime_spec_schema_config_schema *reparsed = NULL; + char *json_buf = NULL; + int ret = 0; + const char *json_with_large_nums = "{" + "\"ociVersion\": \"1.0.0\"," + "\"process\": {" + " \"args\": [\"test\"]," + " \"cwd\": \"/\"," + " \"user\": {\"uid\": 4294967295, \"gid\": 4294967295}" + "}," + "\"root\": {\"path\": \"rootfs\"}" + "}"; + + config = runtime_spec_schema_config_schema_parse_data (json_with_large_nums, 0, &err); + if (config == NULL) + { + printf ("parse with large numbers failed: %s\n", err); + ret = 1; + goto out; + } + + /* Generate and reparse */ + json_buf = runtime_spec_schema_config_schema_generate_json (config, 0, &err); + if (json_buf == NULL) + { + printf ("generate with large numbers failed: %s\n", err); + ret = 2; + goto out; + } + + reparsed = runtime_spec_schema_config_schema_parse_data (json_buf, 0, &err); + if (reparsed == NULL) + { + printf ("reparse with large numbers failed: %s\n", err); + ret = 3; + goto out; + } + + if (config->process->user->uid != reparsed->process->user->uid) + { + printf ("uid mismatch after round-trip\n"); + ret = 4; + goto out; + } + + printf ("large numbers test passed\n"); + +out: + free (err); + free (json_buf); + free_runtime_spec_schema_config_schema (config); + free_runtime_spec_schema_config_schema (reparsed); + return ret; +} + +static int +test_clone_null (void) +{ + runtime_spec_schema_config_schema *cloned = NULL; + int ret = 0; + + /* Clone NULL should return NULL */ + cloned = clone_runtime_spec_schema_config_schema (NULL); + if (cloned != NULL) + { + printf ("expected clone of NULL to return NULL\n"); + free_runtime_spec_schema_config_schema (cloned); + ret = 1; + goto out; + } + + printf ("clone null test passed\n"); + +out: + return ret; +} + +static int +test_free_null (void) +{ + /* Free NULL should not crash */ + free_runtime_spec_schema_config_schema (NULL); + free_image_spec_schema_config_schema (NULL); + + printf ("free null test passed\n"); + return 0; +} + +int +main (void) +{ + int ret; + + ret = test_parse_invalid_json (); + if (ret != 0) + { + printf ("test_parse_invalid_json failed: %d\n", ret); + return ret; + } + + ret = test_parse_minimal_json (); + if (ret != 0) + { + printf ("test_parse_minimal_json failed: %d\n", ret); + return ret; + } + + ret = test_parse_nonexistent_file (); + if (ret != 0) + { + printf ("test_parse_nonexistent_file failed: %d\n", ret); + return ret; + } + + ret = test_null_input (); + if (ret != 0) + { + printf ("test_null_input failed: %d\n", ret); + return ret; + } + + ret = test_empty_arrays (); + if (ret != 0) + { + printf ("test_empty_arrays failed: %d\n", ret); + return ret; + } + + ret = test_string_with_special_chars (); + if (ret != 0) + { + printf ("test_string_with_special_chars failed: %d\n", ret); + return ret; + } + + ret = test_large_numbers (); + if (ret != 0) + { + printf ("test_large_numbers failed: %d\n", ret); + return ret; + } + + ret = test_clone_null (); + if (ret != 0) + { + printf ("test_clone_null failed: %d\n", ret); + return ret; + } + + ret = test_free_null (); + if (ret != 0) + { + printf ("test_free_null failed: %d\n", ret); + return ret; + } + + return 0; +}