| /*****************************************************************************\ | |
| * test_pack_data.c - unit test for pack_data() and unpack_data() | |
| ***************************************************************************** | |
| * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| * | |
| * This file is part of Slurm, a resource management program. | |
| * For details, see <https://slurm.schedmd.com/>. | |
| * Please also read the included file: DISCLAIMER. | |
| * | |
| * Slurm 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 2 of the License, or (at your option) | |
| * any later version. | |
| * | |
| * In addition, as a special exception, the copyright holders give permission | |
| * to link the code of portions of this program with the OpenSSL library under | |
| * certain conditions as described in each individual source file, and | |
| * distribute linked combinations including the two. You must obey the GNU | |
| * General Public License in all respects for all of the code used other than | |
| * OpenSSL. If you modify file(s) with this exception, you may extend this | |
| * exception to your version of the file(s), but you are not obligated to do | |
| * so. If you do not wish to do so, delete this exception statement from your | |
| * version. If you delete this exception statement from all source files in | |
| * the program, then also delete it here. | |
| * | |
| * Slurm 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 Slurm; if not, write to the Free Software Foundation, Inc., | |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| \*****************************************************************************/ | |
| #include <check.h> | |
| #include <errno.h> | |
| #include <float.h> | |
| #include <inttypes.h> | |
| #include <math.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include "slurm/slurm_errno.h" | |
| #include "src/common/data.h" | |
| #include "src/common/log.h" | |
| #include "src/common/macros.h" | |
| #include "src/common/pack.h" | |
| #include "src/common/pack_data.h" | |
| #include "src/common/slurm_protocol_common.h" | |
| #include "src/common/xmalloc.h" | |
| /* | |
| * How deep to probe for the nesting limit. The limit itself is private to | |
| * src/common/pack_data.c, so the tests find it rather than copy it. This only | |
| * has to sit above any limit that could be considered sane. | |
| */ | |
| #define DEPTH_PROBE_LIMIT 128 | |
| /* how many random buffers to throw at unpack_data() */ | |
| #define GARBAGE_ROUNDS 5000 | |
| #define GARBAGE_MAX_BYTES 64 | |
| /* | |
| * The largest entry count that fits a one byte count, so that both it and | |
| * the next one up get packed. | |
| */ | |
| #define COUNT_8_MAX 255 | |
| /* | |
| * Every byte pack_data() writes as a tag, so that half of the garbage starts | |
| * with one and drives the parser past the first byte. Kept here rather than | |
| * shared because data_pack_tag_t is private to src/common/pack_data.c. | |
| */ | |
| static const unsigned char pack_data_tags[] = { | |
| 0x01, /* null */ | |
| 0x02, 0x03, 0x04, /* list, 8 16 and 32 bit counts */ | |
| 0x05, 0x06, 0x07, /* dictionary, 8 16 and 32 bit counts */ | |
| 0x08, 0x09, 0x0a, 0x0b, /* integer, 8 16 32 and 64 bits wide */ | |
| 0x0c, /* string, its own length behind it */ | |
| 0x0d, /* float, 64 bits wide */ | |
| 0x0e, /* bool, 8 bits wide */ | |
| }; | |
| /* every type a data_t can hold, in the order the permutations walk them */ | |
| static const data_type_t pack_data_types[] = { | |
| DATA_TYPE_NULL, DATA_TYPE_LIST, DATA_TYPE_DICT, DATA_TYPE_INT_64, | |
| DATA_TYPE_STRING, DATA_TYPE_FLOAT, DATA_TYPE_BOOL, | |
| }; | |
| /* | |
| * Draw the next pseudo-random number. | |
| * | |
| * Not rand_r(), which only promises the same sequence within one C library. | |
| * The corpus below has to be the same bytes everywhere, or a failure found on | |
| * one platform cannot be repeated on another. | |
| * | |
| * IN/OUT state - generator state, which must not start at zero | |
| * RET the next value | |
| */ | |
| static uint32_t _xorshift32(uint32_t *state) | |
| { | |
| uint32_t x = *state; | |
| x ^= x << 13; | |
| x ^= x >> 17; | |
| x ^= x << 5; | |
| return (*state = x); | |
| } | |
| /* | |
| * Pack data, unpack it into a new data_t, and compare the two. | |
| * | |
| * The comparison is made twice, because neither half catches what the other | |
| * does. data_check_match() converts before it compares, so it sees a lost | |
| * value but not a lost type: an integer and its decimal string match. Packing | |
| * the copy a second time catches that, since the tag differs, along with a | |
| * changed width or a reordered container. pack_data() always writes the same | |
| * bytes for the same data_t, so the two packings are comparable. | |
| * | |
| * IN data - data to round trip | |
| * RET true when the unpacked copy matches what went in | |
| */ | |
| static bool _round_trip(const data_t *data) | |
| { | |
| buf_t *buffer = init_buf(0); | |
| buf_t *again = init_buf(0); | |
| data_t *out = data_new(); | |
| bool match = false; | |
| if (!pack_data(data, SLURM_PROTOCOL_VERSION, buffer)) { | |
| const uint32_t len = get_buf_offset(buffer); | |
| set_buf_offset(buffer, 0); | |
| if (!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer) && | |
| !pack_data(out, SLURM_PROTOCOL_VERSION, again)) | |
| match = (data_check_match(data, out, false) && | |
| (len == get_buf_offset(again)) && | |
| !memcmp(get_buf_data(buffer), | |
| get_buf_data(again), len)); | |
| } | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| FREE_NULL_BUFFER(again); | |
| return match; | |
| } | |
| /* | |
| * Try to unpack bytes that were never packed by pack_data(). | |
| * IN bytes - buffer contents to unpack | |
| * IN len - number of bytes | |
| * RET what unpack_data() returned | |
| */ | |
| static int _unpack_rc(const char *bytes, uint32_t len) | |
| { | |
| char *data = xmalloc(len); | |
| buf_t *buffer; | |
| data_t *out = data_new(); | |
| int rc = EINVAL; | |
| memcpy(data, bytes, len); | |
| buffer = create_buf(data, len); | |
| rc = unpack_data(out, SLURM_PROTOCOL_VERSION, buffer); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| return rc; | |
| } | |
| /* | |
| * Check the bytes pack_data() writes, and that they read back. | |
| * | |
| * The round trip tests cannot see this: pack_data() and unpack_data() move | |
| * together, so renumbering a tag or changing which width a value takes leaves | |
| * every one of them passing. These are the only assertions that pin the | |
| * format itself, which goes on the wire and cannot change. | |
| * | |
| * IN data - data to pack | |
| * IN bytes - exactly what pack_data() must write | |
| * IN len - number of bytes, which may hold a NUL | |
| * IN label - name for the failure message | |
| */ | |
| static void _golden(const data_t *data, const char *bytes, uint32_t len, | |
| const char *label) | |
| { | |
| buf_t *buffer = init_buf(0); | |
| char *copy = xmalloc(len); | |
| buf_t *wire; | |
| data_t *out = data_new(); | |
| ck_assert_msg(!pack_data(data, SLURM_PROTOCOL_VERSION, buffer), | |
| "%s: pack_data() failed", label); | |
| ck_assert_msg(get_buf_offset(buffer) == len, | |
| "%s: packed %u bytes, expected %u", label, | |
| get_buf_offset(buffer), len); | |
| ck_assert_msg(!memcmp(get_buf_data(buffer), bytes, len), | |
| "%s: packed bytes differ from the expected ones", label); | |
| /* and the same bytes, handed back as if they came off the wire */ | |
| memcpy(copy, bytes, len); | |
| wire = create_buf(copy, len); | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, wire), | |
| "%s: unpack_data() failed", label); | |
| ck_assert_msg(data_check_match(data, out, false), | |
| "%s: unpacked value differs", label); | |
| ck_assert_msg(remaining_buf(wire) == 0, "%s: %u bytes left unread", | |
| label, remaining_buf(wire)); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| FREE_NULL_BUFFER(wire); | |
| } | |
| /* | |
| * Check the tag and count a container is packed behind. | |
| * | |
| * A round trip cannot see this: pack_data() and unpack_data() agree on | |
| * whatever width is chosen, so an off-by-one at a count boundary still reads | |
| * back correctly. Only the bytes show which width was picked. | |
| * | |
| * IN data - container to pack | |
| * IN head - the tag and count it must start with | |
| * IN len - length of head | |
| * IN label - name for the failure message | |
| */ | |
| static void _check_head(const data_t *data, const char *head, uint32_t len, | |
| const char *label) | |
| { | |
| buf_t *buffer = init_buf(0); | |
| ck_assert_msg(!pack_data(data, SLURM_PROTOCOL_VERSION, buffer), | |
| "%s: pack_data() failed", label); | |
| ck_assert_msg(get_buf_offset(buffer) >= len, "%s: packed too little", | |
| label); | |
| ck_assert_msg(!memcmp(get_buf_data(buffer), head, len), | |
| "%s: wrong tag or count width", label); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| /* | |
| * Give data the requested type. | |
| * IN/OUT data - data to populate | |
| * IN type - type to give it | |
| * RET the entry to descend into for a list or a dictionary, NULL for the | |
| * types that hold no other data | |
| */ | |
| static data_t *_set_type(data_t *data, data_type_t type) | |
| { | |
| switch (type) { | |
| case DATA_TYPE_NULL: | |
| data_set_null(data); | |
| return NULL; | |
| case DATA_TYPE_INT_64: | |
| data_set_int(data, -12345); | |
| return NULL; | |
| case DATA_TYPE_STRING: | |
| data_set_string(data, "value"); | |
| return NULL; | |
| case DATA_TYPE_FLOAT: | |
| data_set_float(data, 1.5); | |
| return NULL; | |
| case DATA_TYPE_BOOL: | |
| data_set_bool(data, true); | |
| return NULL; | |
| case DATA_TYPE_LIST: | |
| data_set_list(data); | |
| return data_list_append(data); | |
| case DATA_TYPE_DICT: | |
| data_set_dict(data); | |
| return data_key_set(data, "key"); | |
| default: | |
| ck_abort_msg("unexpected type %s", data_type_to_string(type)); | |
| return NULL; | |
| } | |
| } | |
| /* | |
| * Name the shape being walked, for a failure message. | |
| * IN seq - types from the outermost inward | |
| * IN len - how many of them | |
| * RET the shape as text, valid until the next call | |
| */ | |
| static const char *_shape(const data_type_t *seq, int len) | |
| { | |
| static char buf[128]; | |
| size_t n = 0; | |
| buf[0] = '\0'; | |
| for (int i = 0; (i < len) && (n < sizeof(buf)); i++) | |
| n += snprintf(buf + n, sizeof(buf) - n, "%s%s", i ? " > " : "", | |
| data_type_to_string(seq[i])); | |
| return buf; | |
| } | |
| /* | |
| * Nest single entry lists, with a null at the bottom. | |
| * IN/OUT data - data to build the nesting into | |
| * IN levels - how many lists, which must be at least one | |
| */ | |
| static void _nest(data_t *data, int levels) | |
| { | |
| data_t *at = data_set_list(data); | |
| for (int i = 1; i < levels; i++) | |
| at = data_set_list(data_list_append(at)); | |
| data_set_null(data_list_append(at)); | |
| } | |
| START_TEST(test_pack_data_scalars) | |
| { | |
| data_t *data = data_new(); | |
| data_set_null(data); | |
| ck_assert_msg(_round_trip(data), "null"); | |
| data_set_bool(data, true); | |
| ck_assert_msg(_round_trip(data), "bool true"); | |
| data_set_bool(data, false); | |
| ck_assert_msg(_round_trip(data), "bool false"); | |
| data_set_string(data, ""); | |
| ck_assert_msg(_round_trip(data), "empty string"); | |
| data_set_string(data, "a string"); | |
| ck_assert_msg(_round_trip(data), "string"); | |
| data_set_string(data, "\xe2\x82\xac and \xc3\xa9"); | |
| ck_assert_msg(_round_trip(data), "utf-8 string"); | |
| /* a string of the bytes that are also tags */ | |
| data_set_string(data, "\x02\x0c\x0e\x08"); | |
| ck_assert_msg(_round_trip(data), "string of tag bytes"); | |
| data_set_list(data); | |
| ck_assert_msg(_round_trip(data), "empty list"); | |
| data_set_dict(data); | |
| ck_assert_msg(_round_trip(data), "empty dictionary"); | |
| /* an empty key, and one long enough to need more than a byte */ | |
| { | |
| char long_key[300]; | |
| memset(long_key, 'k', sizeof(long_key) - 1); | |
| long_key[sizeof(long_key) - 1] = '\0'; | |
| data_set_dict(data); | |
| data_set_int(data_key_set(data, ""), 1); | |
| ck_assert_msg(_round_trip(data), "empty key"); | |
| data_set_dict(data); | |
| data_set_int(data_key_set(data, long_key), 1); | |
| ck_assert_msg(_round_trip(data), "long key"); | |
| data_set_dict(data); | |
| data_set_int(data_key_set(data, "\xe2\x82\xac"), 1); | |
| ck_assert_msg(_round_trip(data), "utf-8 key"); | |
| /* 127 characters is the last key the short form holds */ | |
| long_key[127] = '\0'; | |
| data_set_dict(data); | |
| data_set_int(data_key_set(data, long_key), 1); | |
| ck_assert_msg(_round_trip(data), "127 character key"); | |
| _check_head(data, "\x05\x01\x7f", 3, "short form key length"); | |
| long_key[127] = 'k'; | |
| long_key[128] = '\0'; | |
| data_set_dict(data); | |
| data_set_int(data_key_set(data, long_key), 1); | |
| ck_assert_msg(_round_trip(data), "128 character key"); | |
| _check_head(data, "\x05\x01\x81\x80", 4, | |
| "long form key length"); | |
| } | |
| FREE_NULL_DATA(data); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_floats) | |
| { | |
| /* | |
| * Values that survive the trip. packdouble() stores the value scaled | |
| * by FLOAT_MULT rather than the bit pattern, so this is not every | |
| * double; see the limit pinned below. | |
| */ | |
| static const double values[] = { | |
| 0.0, -0.0, 1.5, -1.5, 0.1, -0.1, 1e-9, | |
| 100.389, 1e300, -1e300, DBL_MIN, INFINITY, -INFINITY, NAN, | |
| }; | |
| data_t *data = data_new(); | |
| data_t *out = data_new(); | |
| buf_t *buffer = init_buf(0); | |
| for (int i = 0; i < ARRAY_SIZE(values); i++) { | |
| data_set_float(data, values[i]); | |
| ck_assert_msg(_round_trip(data), "float %g", values[i]); | |
| } | |
| /* | |
| * And the limit itself, so that it cannot quietly get worse. Anything | |
| * above DBL_MAX/FLOAT_MULT overflows to infinity on the way in. This | |
| * asserts what the format does today rather than what it should do. | |
| */ | |
| data_set_float(data, DBL_MAX); | |
| ck_assert_msg(!pack_data(data, SLURM_PROTOCOL_VERSION, buffer), | |
| "pack_data() failed"); | |
| set_buf_offset(buffer, 0); | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer), | |
| "unpack_data() failed"); | |
| ck_assert_msg( | |
| isinf(data_get_float(out)), | |
| "DBL_MAX no longer overflows; the FLOAT_MULT limit moved"); | |
| FREE_NULL_DATA(data); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_empty_strings) | |
| { | |
| /* | |
| * The length counts bytes and the terminator is not sent, so nothing | |
| * empty puts a NUL on the wire: an empty string is its tag and a zero | |
| * length, and an empty key is a zero length alone. Only the bytes | |
| * show that, since a round trip is happy either way. | |
| */ | |
| data_t *data = data_new(); | |
| /* | |
| * The empty string alone is pinned by test_pack_data_golden. Start | |
| * with the one byte case, so the framing is not a coincidence, and | |
| * then take the shapes that test does not reach. | |
| */ | |
| _golden(data_set_string(data, "a"), "\x0c\x01\x61", 3, | |
| "one byte string"); | |
| data_set_dict(data); | |
| data_set_int(data_key_set(data, ""), 1); | |
| _golden(data, "\x05\x01\x00\x08\x01", 5, "empty dictionary key"); | |
| data_set_dict(data); | |
| data_set_string(data_key_set(data, ""), ""); | |
| _golden(data, "\x05\x01\x00\x0c\x00", 5, "empty key and value"); | |
| data_set_list(data); | |
| data_set_string(data_list_append(data), ""); | |
| _golden(data, "\x02\x01\x0c\x00", 4, "list holding an empty string"); | |
| /* an empty key does not collide with a one byte key */ | |
| data_set_dict(data); | |
| data_set_int(data_key_set(data, ""), 1); | |
| data_set_int(data_key_set(data, "k"), 2); | |
| /* the empty key took an entry of its own, so the round trip has two */ | |
| ck_assert_int_eq(data_get_dict_length(data), 2); | |
| ck_assert_msg(_round_trip(data), "empty key beside a named one"); | |
| FREE_NULL_DATA(data); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_int_widths) | |
| { | |
| /* | |
| * An integer is packed at the narrowest width that holds it, so walk | |
| * both sides of every width boundary. These are signed, so a value | |
| * that narrows must sign extend back to what it was. | |
| */ | |
| static const int64_t values[] = { | |
| 0, | |
| 1, | |
| -1, | |
| INT8_MAX, | |
| INT8_MIN, | |
| ((int64_t) INT8_MAX + 1), | |
| ((int64_t) INT8_MIN - 1), | |
| INT16_MAX, | |
| INT16_MIN, | |
| ((int64_t) INT16_MAX + 1), | |
| ((int64_t) INT16_MIN - 1), | |
| INT32_MAX, | |
| INT32_MIN, | |
| ((int64_t) INT32_MAX + 1), | |
| ((int64_t) INT32_MIN - 1), | |
| INT64_MAX, | |
| INT64_MIN, | |
| }; | |
| data_t *data = data_new(); | |
| for (int i = 0; i < ARRAY_SIZE(values); i++) { | |
| data_set_int(data, values[i]); | |
| ck_assert_msg(_round_trip(data), "int %" PRId64, values[i]); | |
| } | |
| FREE_NULL_DATA(data); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_golden) | |
| { | |
| data_t *data = data_new(); | |
| _golden(data_set_null(data), "\x01", 1, "null"); | |
| _golden(data_set_bool(data, true), "\x0e\x01", 2, "bool true"); | |
| _golden(data_set_bool(data, false), "\x0e\x00", 2, "bool false"); | |
| /* an integer takes the narrowest width that holds it */ | |
| _golden(data_set_int(data, 0), "\x08\x00", 2, "int 0"); | |
| _golden(data_set_int(data, -1), "\x08\xff", 2, "int -1"); | |
| _golden(data_set_int(data, INT8_MAX), "\x08\x7f", 2, "int 127"); | |
| _golden(data_set_int(data, INT8_MAX + 1), "\x09\x00\x80", 3, "int 128"); | |
| _golden(data_set_int(data, INT16_MAX), "\x09\x7f\xff", 3, "int 32767"); | |
| _golden(data_set_int(data, INT16_MAX + 1), "\x0a\x00\x00\x80\x00", 5, | |
| "int 32768"); | |
| _golden(data_set_int(data, INT32_MAX), "\x0a\x7f\xff\xff\xff", 5, | |
| "int INT32_MAX"); | |
| _golden(data_set_int(data, (int64_t) INT32_MAX + 1), | |
| "\x0b\x00\x00\x00\x00\x80\x00\x00\x00", 9, "int INT32_MAX + 1"); | |
| /* a string carries its length in characters; the NUL is not sent */ | |
| _golden(data_set_string(data, ""), "\x0c\x00", 2, "empty string"); | |
| _golden(data_set_string(data, "hi"), "\x0c\x02\x68\x69", 4, "string"); | |
| /* | |
| * packdouble() stores the value scaled by FLOAT_MULT rather than the | |
| * bit pattern, so these eight bytes are 1.5 * 1000000 as a double. | |
| */ | |
| _golden(data_set_float(data, 1.5), | |
| "\x0d\x41\x36\xe3\x60\x00\x00\x00\x00", 9, "float 1.5"); | |
| _golden(data_set_list(data), "\x02\x00", 2, "empty list"); | |
| _golden(data_set_dict(data), "\x05\x00", 2, "empty dictionary"); | |
| data_set_list(data); | |
| data_set_int(data_list_append(data), 1); | |
| _golden(data, "\x02\x01\x08\x01", 4, "list of one"); | |
| data_set_dict(data); | |
| data_set_bool(data_key_set(data, "k"), true); | |
| _golden(data, "\x05\x01\x01\x6b\x0e\x01", 6, "dictionary of one"); | |
| FREE_NULL_DATA(data); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_counts) | |
| { | |
| /* | |
| * A container count is packed at the narrowest width that holds it, | |
| * so cross the boundary between the 8 and 16 bit counts. The entries | |
| * are distinct, so a reordering is caught as well as a miscount. | |
| * | |
| * The 16 to 32 bit boundary is not covered. It needs about 65535 | |
| * entries, and a container that large is quadratic to release: | |
| * data.c's _release_data_list_node() walks the list to find each | |
| * node's predecessor, so one such list costs about sixteen seconds in | |
| * a developer build. test_unpack_data_wide_counts() reads a 32 bit | |
| * count back instead, which covers the decoding but not the choice of | |
| * width. | |
| */ | |
| static const struct { | |
| uint32_t count; | |
| const char *list_head; | |
| const char *dict_head; | |
| uint32_t head_len; | |
| } counts[] = { | |
| { COUNT_8_MAX, "\x02\xff", "\x05\xff", 2 }, | |
| { (COUNT_8_MAX + 1), "\x03\x01\x00", "\x06\x01\x00", 3 }, | |
| }; | |
| data_t *data = data_new(); | |
| for (int i = 0; i < ARRAY_SIZE(counts); i++) { | |
| data_set_list(data); | |
| for (uint32_t n = 0; n < counts[i].count; n++) | |
| data_set_int(data_list_append(data), n); | |
| ck_assert_msg(_round_trip(data), "list of %u", counts[i].count); | |
| _check_head(data, counts[i].list_head, counts[i].head_len, | |
| "list count width"); | |
| } | |
| for (int i = 0; i < ARRAY_SIZE(counts); i++) { | |
| data_set_dict(data); | |
| for (uint32_t n = 0; n < counts[i].count; n++) { | |
| char key[16]; | |
| snprintf(key, sizeof(key), "%u", n); | |
| data_set_int(data_key_set(data, key), n); | |
| } | |
| ck_assert_msg(_round_trip(data), "dictionary of %u", | |
| counts[i].count); | |
| _check_head(data, counts[i].dict_head, counts[i].head_len, | |
| "dictionary count width"); | |
| } | |
| FREE_NULL_DATA(data); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_string_widths) | |
| { | |
| /* | |
| * A string carries its own length in the same definite length form a | |
| * dictionary key uses, so the short form ends at 127 rather than at | |
| * 255. The length counts bytes and not the terminator, so 127 | |
| * bytes is the last string to fit it. | |
| */ | |
| char str[(COUNT_8_MAX * 2)]; | |
| data_t *data = data_new(); | |
| memset(str, 'x', sizeof(str)); | |
| str[127] = '\0'; | |
| data_set_string(data, str); | |
| ck_assert_msg(_round_trip(data), "string of 127"); | |
| _check_head(data, "\x0c\x7f", 2, "short form string length"); | |
| str[127] = 'x'; | |
| str[128] = '\0'; | |
| data_set_string(data, str); | |
| ck_assert_msg(_round_trip(data), "string of 128"); | |
| _check_head(data, "\x0c\x81\x80", 3, "long form string length"); | |
| /* | |
| * The other two width transitions. Only the bytes show which form | |
| * was written, and the three byte form is the one that splits the | |
| * length by hand rather than handing it to a pack function. | |
| */ | |
| str[128] = 'x'; | |
| str[255] = '\0'; | |
| data_set_string(data, str); | |
| ck_assert_msg(_round_trip(data), "string of 255"); | |
| _check_head(data, "\x0c\x81\xff", 3, "last one byte length"); | |
| str[255] = 'x'; | |
| str[256] = '\0'; | |
| data_set_string(data, str); | |
| ck_assert_msg(_round_trip(data), "string of 256"); | |
| _check_head(data, "\x0c\x82\x01\x00", 4, "first two byte length"); | |
| { | |
| char *big = xmalloc(0x10001); | |
| memset(big, 'x', 0xffff); | |
| data_set_string(data, big); | |
| ck_assert_msg(_round_trip(data), "string of 65535"); | |
| _check_head(data, "\x0c\x82\xff\xff", 4, | |
| "last two byte length"); | |
| memset(big, 'x', 0x10000); | |
| data_set_string(data, big); | |
| ck_assert_msg(_round_trip(data), "string of 65536"); | |
| _check_head(data, "\x0c\x83\x01\x00\x00", 5, | |
| "first three byte length"); | |
| xfree(big); | |
| } | |
| /* a zero length is an empty string, and stays a string */ | |
| { | |
| char *copy = xmalloc(2); | |
| buf_t *buffer; | |
| data_t *out = data_new(); | |
| memcpy(copy, "\x0c\x00", 2); | |
| buffer = create_buf(copy, 2); | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer), | |
| "zero length string was rejected"); | |
| ck_assert_msg(data_get_type(out) == DATA_TYPE_STRING, | |
| "zero length string did not stay a string"); | |
| ck_assert_str_eq(data_get_string(out), ""); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| FREE_NULL_DATA(data); | |
| } | |
| END_TEST | |
| START_TEST(test_unpack_data_key_lengths) | |
| { | |
| /* | |
| * A key length is written the way X.690 writes a definite length: a | |
| * byte under 128 is the length, and above it the low bits count the | |
| * bytes that follow. Only the shortest form of a length is valid, so | |
| * one length has one spelling on the wire and a message that is | |
| * unpacked and packed again comes back the same. | |
| */ | |
| ck_assert_int_eq(_unpack_rc("\x05\x01\x81\x01\x6b\x0e\x01", 7), | |
| SLURM_ERROR); | |
| ck_assert_int_eq(_unpack_rc("\x05\x01\x82\x00\x01\x6b\x0e\x01", 8), | |
| SLURM_ERROR); | |
| /* the indefinite form, which is not a length at all */ | |
| ck_assert_int_eq(_unpack_rc("\x05\x01\x80\x6b\x0e\x01", 6), | |
| SLURM_ERROR); | |
| /* | |
| * Wider than four bytes, which would not fit a uint32_t. The length | |
| * below is otherwise well formed and the key that follows it is | |
| * complete, so nothing but the width check can reject this. Reading | |
| * five bytes would also shift a uint32_t by 32, which is undefined. | |
| */ | |
| { | |
| char wide[(2 + 6 + 128 + 2)]; | |
| int n = 0; | |
| wide[n++] = 0x05; /* DATA_TAG_DICT_8 */ | |
| wide[n++] = 0x01; /* one entry */ | |
| wide[n++] = 0x85; /* five length bytes follow */ | |
| wide[n++] = 0x00; | |
| wide[n++] = 0x00; | |
| wide[n++] = 0x00; | |
| wide[n++] = 0x00; | |
| wide[n++] = 0x80; /* a key length of 128 */ | |
| for (int i = 0; i < 128; i++) | |
| wide[n++] = 'k'; | |
| wide[n++] = 0x0e; /* DATA_TAG_BOOL_8 */ | |
| wide[n++] = 0x01; | |
| ck_assert_int_eq(_unpack_rc(wide, n), SLURM_ERROR); | |
| } | |
| /* | |
| * The terminator is not sent, so a NUL among the bytes was not put | |
| * there by pack_data(). Left in, it would truncate the string, and two | |
| * dictionary keys differing only past a NUL would collide on the | |
| * shorter of the two. | |
| */ | |
| ck_assert_int_eq(_unpack_rc("\x0c\x05\x41\x42\x00\x43\x44", 7), | |
| SLURM_ERROR); | |
| ck_assert_int_eq( | |
| _unpack_rc("\x05\x02\x01\x61\x08\x01\x03\x61\x00\x62\x08\x02", | |
| 12), | |
| SLURM_ERROR); | |
| /* | |
| * The same key twice. data_key_set() would hand back the first | |
| * entry, so the second value would replace it and the dictionary | |
| * would come back shorter than the wire said. | |
| */ | |
| ck_assert_int_eq(_unpack_rc("\x05\x02\x01\x6b\x08\x01\x01\x6b\x08\x02", | |
| 10), | |
| SLURM_ERROR); | |
| /* the sentinels, which are reserved rather than sizes */ | |
| ck_assert_int_eq(_unpack_rc("\x05\x01\x84\xff\xff\xff\xff", 7), | |
| SLURM_ERROR); | |
| ck_assert_int_eq(_unpack_rc("\x05\x01\x84\xff\xff\xff\xfe", 7), | |
| SLURM_ERROR); | |
| } | |
| END_TEST | |
| START_TEST(test_unpack_data_wide_counts) | |
| { | |
| /* | |
| * A count wider than the entries need is still a count. pack_data() | |
| * only writes the narrowest one, so these come from a peer that chose | |
| * otherwise, and they are the only coverage of reading a 32 bit | |
| * count. | |
| */ | |
| data_t *out = data_new(); | |
| data_t *entry = NULL; | |
| /* a 32 bit list count, holding two entries */ | |
| { | |
| const char bytes[] = "\x04\x00\x00\x00\x02\x08\x01\x08\x02"; | |
| const uint32_t len = sizeof(bytes) - 1; | |
| char *copy = xmalloc(len); | |
| buf_t *buffer; | |
| memcpy(copy, bytes, len); | |
| buffer = create_buf(copy, len); | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer), | |
| "32 bit list count was rejected"); | |
| ck_assert_msg(data_get_type(out) == DATA_TYPE_LIST, | |
| "not a list"); | |
| ck_assert_int_eq(data_get_list_length(out), 2); | |
| ck_assert_msg(remaining_buf(buffer) == 0, "bytes left unread"); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| /* a 32 bit dictionary count, holding one entry */ | |
| { | |
| const char bytes[] = "\x07\x00\x00\x00\x01\x01\x6b\x0e\x01"; | |
| const uint32_t len = sizeof(bytes) - 1; | |
| char *copy = xmalloc(len); | |
| buf_t *buffer; | |
| memcpy(copy, bytes, len); | |
| buffer = create_buf(copy, len); | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer), | |
| "32 bit dictionary count was rejected"); | |
| ck_assert_msg(data_get_type(out) == DATA_TYPE_DICT, | |
| "not a dictionary"); | |
| ck_assert_msg((entry = data_key_get(out, "k")), | |
| "key is missing"); | |
| ck_assert_msg(data_get_bool(entry), "value is wrong"); | |
| ck_assert_msg(remaining_buf(buffer) == 0, "bytes left unread"); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| FREE_NULL_DATA(out); | |
| } | |
| END_TEST | |
| /* | |
| * Round trip every shape that fits within the given depth. | |
| * | |
| * A list or a dictionary descends one level further; any other type ends the | |
| * shape there. Each shape is packed as it is reached, so the shorter ones are | |
| * covered on the way down rather than by repeating the walk. | |
| * | |
| * IN/OUT root - outermost data, packed once per shape | |
| * IN/OUT at - entry to give the next type to | |
| * IN/OUT seq - types so far, for the failure message | |
| * IN level - how deep at sits | |
| * IN max - deepest level to build | |
| */ | |
| static void _walk_shapes(const data_t *root, data_t *at, data_type_t *seq, | |
| int level, int max) | |
| { | |
| if (level >= max) | |
| return; | |
| for (int i = 0; i < ARRAY_SIZE(pack_data_types); i++) { | |
| data_t *next = NULL; | |
| seq[level] = pack_data_types[i]; | |
| next = _set_type(at, seq[level]); | |
| ck_assert_msg(_round_trip(root), "%s", | |
| _shape(seq, (level + 1))); | |
| if (next) | |
| _walk_shapes(root, next, seq, (level + 1), max); | |
| } | |
| } | |
| START_TEST(test_pack_data_depth) | |
| { | |
| data_t *data = data_new(); | |
| data_type_t seq[4] = { 0 }; | |
| _walk_shapes(data, data, seq, 0, ARRAY_SIZE(seq)); | |
| FREE_NULL_DATA(data); | |
| } | |
| END_TEST | |
| /* | |
| * Find the deepest nesting pack_data() accepts. | |
| * | |
| * The limit is private to pack_data.c. Probing for it keeps the assertions | |
| * about behaviour rather than about a constant the test cannot see, and means | |
| * a copy of it cannot drift. | |
| * | |
| * RET the deepest level that packed | |
| */ | |
| static int _find_max_depth(void) | |
| { | |
| data_t *data = data_new(); | |
| int deepest = 0; | |
| for (int levels = 1; levels <= DEPTH_PROBE_LIMIT; levels++) { | |
| buf_t *buffer = init_buf(0); | |
| int rc = EINVAL; | |
| _nest(data, levels); | |
| rc = pack_data(data, SLURM_PROTOCOL_VERSION, buffer); | |
| FREE_NULL_BUFFER(buffer); | |
| if (rc) | |
| break; | |
| deepest = levels; | |
| } | |
| FREE_NULL_DATA(data); | |
| return deepest; | |
| } | |
| START_TEST(test_pack_data_max_depth) | |
| { | |
| const int deepest = _find_max_depth(); | |
| data_t *data = data_new(); | |
| buf_t *buffer = init_buf(0); | |
| /* | |
| * Too shallow and a legitimately nested payload stops fitting; too | |
| * deep and the recursion is no longer bounded well short of the | |
| * stack. The band is what notices the limit being moved. | |
| */ | |
| ck_assert_msg((deepest >= 8) && (deepest <= 64), | |
| "pack_data() accepts %d levels, outside the usable band", | |
| deepest); | |
| /* whatever it accepts has to survive the trip back */ | |
| _nest(data, deepest); | |
| ck_assert_msg(_round_trip(data), "nesting at the limit was refused"); | |
| /* | |
| * One deeper is refused while packing, rather than written into a | |
| * buffer that unpack_data() would always reject. | |
| */ | |
| _nest(data, (deepest + 1)); | |
| ck_assert_int_eq(pack_data(data, SLURM_PROTOCOL_VERSION, buffer), | |
| SLURM_ERROR); | |
| FREE_NULL_DATA(data); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_null_args) | |
| { | |
| buf_t *buffer = init_buf(0); | |
| data_t *data = data_set_int(data_new(), 1); | |
| ck_assert_int_eq(pack_data(NULL, SLURM_PROTOCOL_VERSION, NULL), EINVAL); | |
| ck_assert_int_eq(pack_data(data, SLURM_PROTOCOL_VERSION, NULL), EINVAL); | |
| ck_assert_int_eq(unpack_data(NULL, SLURM_PROTOCOL_VERSION, buffer), | |
| EINVAL); | |
| ck_assert_int_eq(unpack_data(data, SLURM_PROTOCOL_VERSION, NULL), | |
| EINVAL); | |
| FREE_NULL_DATA(data); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_null_ptr) | |
| { | |
| data_t *null = data_set_null(data_new()); | |
| data_t *out = data_set_int(data_new(), 1); | |
| buf_t *by_ptr = init_buf(0); | |
| buf_t *by_val = init_buf(0); | |
| /* NULL is packed exactly as a data_t holding null */ | |
| ck_assert_int_eq(pack_data(NULL, SLURM_PROTOCOL_VERSION, by_ptr), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(pack_data(null, SLURM_PROTOCOL_VERSION, by_val), | |
| SLURM_SUCCESS); | |
| ck_assert_int_eq(get_buf_offset(by_ptr), get_buf_offset(by_val)); | |
| ck_assert(!memcmp(get_buf_data(by_ptr), get_buf_data(by_val), | |
| get_buf_offset(by_val))); | |
| /* and comes back as null, replacing whatever was there */ | |
| set_buf_offset(by_ptr, 0); | |
| ck_assert_int_eq(unpack_data(out, SLURM_PROTOCOL_VERSION, by_ptr), | |
| SLURM_SUCCESS); | |
| ck_assert(data_get_type(out) == DATA_TYPE_NULL); | |
| FREE_NULL_DATA(null); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(by_ptr); | |
| FREE_NULL_BUFFER(by_val); | |
| } | |
| END_TEST | |
| START_TEST(test_pack_data_adjacent) | |
| { | |
| /* | |
| * A data_t has to sit in a buffer beside other fields, which is the | |
| * reason for packing one at all. Nothing else here would catch an | |
| * offset that moved too far or not far enough. | |
| */ | |
| buf_t *buffer = init_buf(0); | |
| data_t *first = data_set_string(data_new(), "first"); | |
| data_t *second = data_set_int(data_new(), -12345); | |
| data_t *out = data_new(); | |
| uint32_t head = 0, tail = 0, packed = 0; | |
| pack32(0xdeadbeef, buffer); | |
| ck_assert_msg(!pack_data(first, SLURM_PROTOCOL_VERSION, buffer), | |
| "first pack_data() failed"); | |
| ck_assert_msg(!pack_data(second, SLURM_PROTOCOL_VERSION, buffer), | |
| "second pack_data() failed"); | |
| pack32(0xcafebabe, buffer); | |
| /* | |
| * What was written, which is not size_buf(): init_buf() allocates | |
| * ahead, so the buffer is far larger than the bytes put in it. | |
| */ | |
| packed = get_buf_offset(buffer); | |
| set_buf_offset(buffer, 0); | |
| ck_assert_msg(!unpack32(&head, buffer), "leading sentinel"); | |
| ck_assert_int_eq(head, 0xdeadbeef); | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer), | |
| "first unpack_data() failed"); | |
| ck_assert_msg(data_check_match(first, out, false), "first value"); | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer), | |
| "second unpack_data() failed"); | |
| ck_assert_msg(data_check_match(second, out, false), "second value"); | |
| ck_assert_msg(!unpack32(&tail, buffer), "trailing sentinel"); | |
| ck_assert_int_eq(tail, 0xcafebabe); | |
| ck_assert_msg(get_buf_offset(buffer) == packed, | |
| "read %u of the %u bytes written", get_buf_offset(buffer), | |
| packed); | |
| FREE_NULL_DATA(first); | |
| FREE_NULL_DATA(second); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| END_TEST | |
| START_TEST(test_unpack_data_reuse) | |
| { | |
| /* | |
| * unpack_data() takes an existing data_t, so a caller reading several | |
| * values out of one buffer will hand it the same one each time. What | |
| * it held before must go. | |
| */ | |
| buf_t *buffer = init_buf(0); | |
| data_t *dict = data_set_dict(data_new()); | |
| data_t *scalar = data_set_int(data_new(), 7); | |
| data_t *out = data_new(); | |
| data_set_int(data_key_set(dict, "one"), 1); | |
| data_set_int(data_key_set(dict, "two"), 2); | |
| ck_assert_msg(!pack_data(dict, SLURM_PROTOCOL_VERSION, buffer), | |
| "pack_data() failed"); | |
| ck_assert_msg(!pack_data(scalar, SLURM_PROTOCOL_VERSION, buffer), | |
| "pack_data() failed"); | |
| set_buf_offset(buffer, 0); | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer), | |
| "unpack_data() failed"); | |
| ck_assert_msg(data_check_match(dict, out, false), "dictionary"); | |
| /* the same data_t again, this time taking a scalar */ | |
| ck_assert_msg(!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer), | |
| "unpack_data() failed"); | |
| ck_assert_msg(data_get_type(out) == DATA_TYPE_INT_64, | |
| "reused data_t kept its old type"); | |
| ck_assert_int_eq(data_get_int(out), 7); | |
| FREE_NULL_DATA(dict); | |
| FREE_NULL_DATA(scalar); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| END_TEST | |
| START_TEST(test_unpack_data_malformed) | |
| { | |
| /* 0x00 and 0xff are deliberately not tags */ | |
| ck_assert_int_eq(_unpack_rc("\x00", 1), SLURM_ERROR); | |
| ck_assert_int_eq(_unpack_rc("\xff", 1), SLURM_ERROR); | |
| ck_assert_int_eq(_unpack_rc("\x0f", 1), SLURM_ERROR); | |
| /* a tag with the value it promised missing */ | |
| ck_assert_int_eq(_unpack_rc("\x0b", 1), SLURM_ERROR); | |
| ck_assert_int_eq(_unpack_rc("\x0c\x05\x68", 3), SLURM_ERROR); | |
| /* containers claiming more entries than the buffer can hold */ | |
| ck_assert_int_eq(_unpack_rc("\x04\xff\xff\xff\xff", 5), SLURM_ERROR); | |
| ck_assert_int_eq(_unpack_rc("\x02\xc8", 2), SLURM_ERROR); | |
| ck_assert_int_eq(_unpack_rc("\x05\x01", 2), SLURM_ERROR); | |
| /* a dictionary entry with a key but no value */ | |
| ck_assert_int_eq(_unpack_rc("\x05\x01\x01\x6b", 4), SLURM_ERROR); | |
| } | |
| END_TEST | |
| START_TEST(test_unpack_data_too_many) | |
| { | |
| /* | |
| * Depth bounds how deep a message goes, not how wide. Every entry | |
| * costs a data_t and a list node that data.c takes with xmalloc() and | |
| * cannot refuse, and one wire byte asks for another, so a message | |
| * that is small and entirely well formed can still ask for more than | |
| * the heap will give. The counts below are honest, which is the | |
| * point: nothing but the entry budget can refuse the larger one. | |
| * | |
| * The budget is private to pack_data.c, so bracket it rather than | |
| * name it: a thousand entries is a payload anything would take, and | |
| * a million is one nothing should. | |
| */ | |
| const uint32_t few = 1000, many = 1000000; | |
| char *wire = xmalloc(5 + many); | |
| wire[0] = 0x04; /* DATA_TAG_LIST_32 */ | |
| memset(&wire[5], 0x01, many); /* DATA_TAG_NULL each */ | |
| wire[1] = (few >> 24); | |
| wire[2] = (few >> 16); | |
| wire[3] = (few >> 8); | |
| wire[4] = few; | |
| ck_assert_int_eq(_unpack_rc(wire, (5 + few)), SLURM_SUCCESS); | |
| wire[1] = (many >> 24); | |
| wire[2] = (many >> 16); | |
| wire[3] = (many >> 8); | |
| wire[4] = many; | |
| ck_assert_int_eq(_unpack_rc(wire, (5 + many)), SLURM_ERROR); | |
| xfree(wire); | |
| } | |
| END_TEST | |
| START_TEST(test_unpack_data_too_deep) | |
| { | |
| /* | |
| * Single entry lists one level past what unpack_data() accepts. | |
| * pack_data() will not write this, so the bytes are built here. | |
| */ | |
| const int levels = (_find_max_depth() + 1); | |
| char *nested = xmalloc((levels * 2) + 1); | |
| int i = 0; | |
| for (int n = 0; n < levels; n++) { | |
| nested[i++] = 0x02; /* DATA_TAG_LIST_8 */ | |
| nested[i++] = 0x01; /* one entry */ | |
| } | |
| nested[i++] = 0x01; /* DATA_TAG_NULL */ | |
| ck_assert_int_eq(_unpack_rc(nested, i), SLURM_ERROR); | |
| xfree(nested); | |
| } | |
| END_TEST | |
| START_TEST(test_safe_unpack_data) | |
| { | |
| buf_t *buffer = init_buf(0); | |
| data_t *in = data_set_string(data_new(), "through the macro"); | |
| data_t *out = NULL; | |
| ck_assert_msg(!pack_data(in, SLURM_PROTOCOL_VERSION, buffer), | |
| "pack_data() failed"); | |
| set_buf_offset(buffer, 0); | |
| safe_unpack_data(out, SLURM_PROTOCOL_VERSION, buffer); | |
| ck_assert_msg(data_check_match(in, out, false), | |
| "safe_unpack_data() did not give back what went in"); | |
| FREE_NULL_DATA(in); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| return; | |
| unpack_error: | |
| FREE_NULL_DATA(in); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| ck_abort_msg("safe_unpack_data() took the unpack_error path"); | |
| } | |
| END_TEST | |
| START_TEST(test_safe_unpack_data_null) | |
| { | |
| buf_t *buffer = init_buf(0); | |
| data_t *out = NULL; | |
| data_t *keep = data_set_int(data_new(), 1); | |
| ck_assert(!pack_data(NULL, SLURM_PROTOCOL_VERSION, buffer)); | |
| ck_assert(!pack_data(NULL, SLURM_PROTOCOL_VERSION, buffer)); | |
| set_buf_offset(buffer, 0); | |
| /* NULL valp: null comes back as NULL, nothing leaked */ | |
| safe_unpack_data(out, SLURM_PROTOCOL_VERSION, buffer); | |
| ck_assert_msg(!out, "NULL valp unpacking null was not reset to NULL"); | |
| /* caller-owned valp: kept, set to null, not freed */ | |
| safe_unpack_data(keep, SLURM_PROTOCOL_VERSION, buffer); | |
| ck_assert_msg(keep, "caller-owned data_t was freed"); | |
| ck_assert(data_get_type(keep) == DATA_TYPE_NULL); | |
| FREE_NULL_DATA(keep); | |
| FREE_NULL_BUFFER(buffer); | |
| return; | |
| unpack_error: | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_DATA(keep); | |
| FREE_NULL_BUFFER(buffer); | |
| ck_abort_msg("safe_unpack_data() took the unpack_error path"); | |
| } | |
| END_TEST | |
| START_TEST(test_unpack_data_garbage) | |
| { | |
| /* a fixed seed, so that anything this finds can be repeated */ | |
| uint32_t seed = 0x5a5a5a5a; | |
| for (int i = 0; i < GARBAGE_ROUNDS; i++) { | |
| char bytes[GARBAGE_MAX_BYTES]; | |
| uint32_t len = 1 + (_xorshift32(&seed) % sizeof(bytes)); | |
| char *copy = xmalloc(len); | |
| buf_t *buffer; | |
| data_t *out = data_new(); | |
| for (uint32_t b = 0; b < len; b++) | |
| bytes[b] = _xorshift32(&seed); | |
| /* half of them open with a tag that unpack_data() knows */ | |
| if (i % 2) | |
| bytes[0] = pack_data_tags[_xorshift32(&seed) % | |
| ARRAY_SIZE(pack_data_tags)]; | |
| memcpy(copy, bytes, len); | |
| buffer = create_buf(copy, len); | |
| /* | |
| * Random bytes can be a valid packing by chance, so the return | |
| * is not asserted on. An accepted buffer has to repack. A | |
| * rejected one is only required to return at all: what the | |
| * round buys is that unpack_data() did not crash, abort, | |
| * corrupt the heap or hang, which libcheck's fork mode and | |
| * timeout report on their own. | |
| * | |
| * Staying inside the buffer is deliberately not asserted here. | |
| * processed <= size is an invariant of the buf_t API, so the | |
| * assertion could not fail, and an overread that never | |
| * advances processed would satisfy it anyway. Run the corpus | |
| * under a sanitizer to check bounds. | |
| */ | |
| if (!unpack_data(out, SLURM_PROTOCOL_VERSION, buffer)) | |
| ck_assert_msg(_round_trip(out), | |
| "round %d cannot be repacked", i); | |
| FREE_NULL_DATA(out); | |
| FREE_NULL_BUFFER(buffer); | |
| } | |
| } | |
| END_TEST | |
| /* | |
| * Keep the rejection logging out of the test output. | |
| * | |
| * The error path tests reject thousands of buffers, and every one of them is | |
| * logged. Done as a fixture so that the level is put back even when an | |
| * assertion ends the test early, which matters under CK_FORK=no. | |
| */ | |
| static void _quiet_setup(void) | |
| { | |
| log_options_t quiet = LOG_OPTS_INITIALIZER; | |
| quiet.stderr_level = LOG_LEVEL_FATAL; | |
| log_alter(quiet, 0, NULL); | |
| } | |
| static void _quiet_teardown(void) | |
| { | |
| log_options_t loud = LOG_OPTS_INITIALIZER; | |
| loud.stderr_level = LOG_LEVEL_DEBUG5; | |
| log_alter(loud, 0, NULL); | |
| } | |
| extern int main(int argc, char **argv) | |
| { | |
| int failures; | |
| log_options_t log_opts = LOG_OPTS_INITIALIZER; | |
| TCase *tcase = tcase_create("pack_data"); | |
| TCase *tcase_errors = tcase_create("pack_data_errors"); | |
| Suite *suite = suite_create("pack_data"); | |
| SRunner *sr = NULL; | |
| log_opts.stderr_level = LOG_LEVEL_DEBUG5; | |
| log_init("test_pack_data", log_opts, 0, NULL); | |
| tcase_add_test(tcase, test_pack_data_scalars); | |
| tcase_add_test(tcase, test_pack_data_floats); | |
| tcase_add_test(tcase, test_pack_data_empty_strings); | |
| tcase_add_test(tcase, test_pack_data_int_widths); | |
| tcase_add_test(tcase, test_pack_data_golden); | |
| tcase_add_test(tcase, test_pack_data_counts); | |
| tcase_add_test(tcase, test_pack_data_string_widths); | |
| tcase_add_test(tcase, test_unpack_data_wide_counts); | |
| tcase_add_test(tcase, test_pack_data_depth); | |
| tcase_add_test(tcase, test_pack_data_max_depth); | |
| tcase_add_test(tcase, test_pack_data_null_args); | |
| tcase_add_test(tcase, test_pack_data_null_ptr); | |
| tcase_add_test(tcase, test_pack_data_adjacent); | |
| tcase_add_test(tcase, test_unpack_data_reuse); | |
| tcase_add_test(tcase, test_safe_unpack_data); | |
| tcase_add_test(tcase, test_safe_unpack_data_null); | |
| /* | |
| * The permutation walk and the count tests are not quick, but the | |
| * harness kills the whole binary at atf.default_command_timeout, | |
| * which is sixty seconds. Stay below it, so an overrun is reported | |
| * as a named test failure instead of a kill with no results. | |
| */ | |
| tcase_set_timeout(tcase, 50); | |
| suite_add_tcase(suite, tcase); | |
| /* | |
| * The rejection tests are noisy, so they get their own case with a | |
| * fixture that mutes the log and puts it back however the test ends. | |
| */ | |
| tcase_add_checked_fixture(tcase_errors, _quiet_setup, _quiet_teardown); | |
| tcase_add_test(tcase_errors, test_unpack_data_key_lengths); | |
| tcase_add_test(tcase_errors, test_unpack_data_malformed); | |
| tcase_add_test(tcase_errors, test_unpack_data_too_deep); | |
| tcase_add_test(tcase_errors, test_unpack_data_too_many); | |
| tcase_add_test(tcase_errors, test_unpack_data_garbage); | |
| tcase_set_timeout(tcase_errors, 50); | |
| suite_add_tcase(suite, tcase_errors); | |
| sr = srunner_create(suite); | |
| srunner_run_all(sr, CK_VERBOSE); | |
| failures = srunner_ntests_failed(sr); | |
| srunner_free(sr); | |
| return failures; | |
| } |