diff --git a/src/sas/readstat_sas7bcat_read.c b/src/sas/readstat_sas7bcat_read.c index 9659d326..1d9c08eb 100644 --- a/src/sas/readstat_sas7bcat_read.c +++ b/src/sas/readstat_sas7bcat_read.c @@ -195,7 +195,7 @@ static readstat_error_t sas7bcat_parse_block(const char *data, size_t data_size, int string_offset = 0; int payload_offset = 106; uint16_t flags = 0; - char name[4*32+1]; + char name[4*32+2]; /* room for the trailing dot */ if (data_size < payload_offset) goto cleanup; @@ -227,7 +227,7 @@ static readstat_error_t sas7bcat_parse_block(const char *data, size_t data_size, goto cleanup; } - if ((retval = readstat_convert(name, sizeof(name), &data[8], 8, ctx->converter)) != READSTAT_OK) + if ((retval = readstat_convert(name, sizeof(name) - 1, &data[8], 8, ctx->converter)) != READSTAT_OK) goto cleanup; if (pad) { @@ -238,12 +238,15 @@ static readstat_error_t sas7bcat_parse_block(const char *data, size_t data_size, if (data_size < payload_offset + pad + 32) goto cleanup; - retval = readstat_convert(name, sizeof(name), &data[payload_offset+pad], 32, ctx->converter); + retval = readstat_convert(name, sizeof(name) - 1, &data[payload_offset+pad], 32, ctx->converter); if (retval != READSTAT_OK) goto cleanup; pad += 32; } + /* Present format names the way SAS does, e.g. "MYFMT." or "$MYFMT." */ + strcat(name, "."); + if (data_size < payload_offset + pad) goto cleanup; diff --git a/src/sas/readstat_sas7bcat_write.c b/src/sas/readstat_sas7bcat_write.c index 3528634c..10f27daf 100644 --- a/src/sas/readstat_sas7bcat_write.c +++ b/src/sas/readstat_sas7bcat_write.c @@ -14,18 +14,58 @@ typedef struct sas7bcat_block_s { char data[1]; // Flexible array; use [1] for C++-98 compatibility } sas7bcat_block_t; +/* Validates a catalog format name such as "MYFMT", "MYFMT.", "$MYFMT" or + * "$MYFMT." and returns the length of the name without the trailing dot. + * Widths and decimals are not part of a format's name in a catalog, so a name + * ending in a digit is rejected, as are names whose '$' prefix doesn't match + * the type of the label set. */ +static readstat_error_t sas7bcat_validate_format_name(readstat_label_set_t *r_label_set, + size_t *name_len_out) { + const char *name = r_label_set->name; + size_t len = strlen(name); + size_t i = 0; + + if (len && name[len-1] == '.') + len--; + + if (len > 32) + return READSTAT_ERROR_BAD_FORMAT_STRING; + + int is_string = (len > 0 && name[0] == '$'); + if (is_string != (r_label_set->type == READSTAT_TYPE_STRING)) + return READSTAT_ERROR_BAD_FORMAT_STRING; + if (is_string) + i++; + + if (i == len) + return READSTAT_ERROR_BAD_FORMAT_STRING; + + char c = name[i]; + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')) + return READSTAT_ERROR_BAD_FORMAT_STRING; + + for (i++; i= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '_')) + return READSTAT_ERROR_BAD_FORMAT_STRING; + } + + if (name[len-1] >= '0' && name[len-1] <= '9') + return READSTAT_ERROR_BAD_FORMAT_STRING; + + *name_len_out = len; + return READSTAT_OK; +} + static sas7bcat_block_t *sas7bcat_block_for_label_set( - readstat_label_set_t *r_label_set, readstat_writer_t *writer) { + readstat_label_set_t *r_label_set, size_t name_len, readstat_writer_t *writer) { size_t len = 106; - size_t name_len = strlen(r_label_set->name); int j; char name[32]; if (name_len > 8) { len += 32; // long name - if (name_len > 32) { - name_len = 32; - } } if (writer->is_64bit) { len += 32; @@ -147,11 +187,15 @@ static readstat_error_t sas7bcat_begin_data(void *writer_ctx) { int i; sas_header_info_t *hinfo = sas_header_info_init(writer, writer->is_64bit); - sas7bcat_block_t **blocks = malloc(writer->label_sets_count * sizeof(sas7bcat_block_t)); + sas7bcat_block_t **blocks = calloc(writer->label_sets_count, sizeof(sas7bcat_block_t *)); char *page = malloc(hinfo->page_size); for (i=0; ilabel_sets_count; i++) { - blocks[i] = sas7bcat_block_for_label_set(writer->label_sets[i], writer); + size_t name_len = 0; + retval = sas7bcat_validate_format_name(writer->label_sets[i], &name_len); + if (retval != READSTAT_OK) + goto cleanup; + blocks[i] = sas7bcat_block_for_label_set(writer->label_sets[i], name_len, writer); } hinfo->page_count = 4; diff --git a/src/test/test_list.h b/src/test/test_list.h index 902936ff..24ad793e 100644 --- a/src/test/test_list.h +++ b/src/test/test_list.h @@ -2287,7 +2287,7 @@ static rt_test_group_t _test_groups[] = { .label_sets_count = 1, .label_sets = { { - .name = "LabelSet", + .name = "LabelSet.", .type = READSTAT_TYPE_DOUBLE, .value_labels_count = 2, .value_labels = { @@ -2310,7 +2310,7 @@ static rt_test_group_t _test_groups[] = { .label_sets_count = 1, .label_sets = { { - .name = "$StringLabelSet", + .name = "$StringLabelSet.", .type = READSTAT_TYPE_STRING, .value_labels_count = 4, .value_labels = { @@ -2341,7 +2341,7 @@ static rt_test_group_t _test_groups[] = { .label_sets_count = 2, .label_sets = { { - .name = "OneTwo", + .name = "OneTwo.", .type = READSTAT_TYPE_DOUBLE, .value_labels_count = 2, .value_labels = { @@ -2356,7 +2356,7 @@ static rt_test_group_t _test_groups[] = { } }, { - .name = "ThreeFour", + .name = "ThreeFour.", .type = READSTAT_TYPE_DOUBLE, .value_labels_count = 2, .value_labels = { @@ -2373,6 +2373,125 @@ static rt_test_group_t _test_groups[] = { } }, + { + .label = "SAS string value labels with dotted long name", + .test_formats = RT_FORMAT_SAS7BCAT, + .label_sets_count = 1, + .label_sets = { + { + .name = "$ABCDEFGHIJKLMNOPQRSTUVWXYZABCDE.", + .type = READSTAT_TYPE_STRING, + .value_labels_count = 1, + .value_labels = { + { + .value = { .type = READSTAT_TYPE_STRING, .v = { .string_value = "A" } }, + .label = "Apple" + } + } + } + } + }, + + { + .label = "SAS catalog format name with width", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS7BCAT, + .label_sets_count = 1, + .label_sets = { + { + .name = "MYFMT8.", + .type = READSTAT_TYPE_DOUBLE, + .value_labels_count = 1, + .value_labels = { + { + .value = { .type = READSTAT_TYPE_DOUBLE, .v = { .double_value = 1.0 } }, + .label = "One" + } + } + } + } + }, + + { + .label = "SAS catalog format name with width and digits", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS7BCAT, + .label_sets_count = 1, + .label_sets = { + { + .name = "MYFMT8.2", + .type = READSTAT_TYPE_DOUBLE, + .value_labels_count = 1, + .value_labels = { + { + .value = { .type = READSTAT_TYPE_DOUBLE, .v = { .double_value = 1.0 } }, + .label = "One" + } + } + } + } + }, + + { + .label = "SAS catalog numeric format with $ name", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS7BCAT, + .label_sets_count = 1, + .label_sets = { + { + .name = "$MYFMT.", + .type = READSTAT_TYPE_DOUBLE, + .value_labels_count = 1, + .value_labels = { + { + .value = { .type = READSTAT_TYPE_DOUBLE, .v = { .double_value = 1.0 } }, + .label = "One" + } + } + } + } + }, + + { + .label = "SAS catalog string format without $ name", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS7BCAT, + .label_sets_count = 1, + .label_sets = { + { + .name = "MYFMT", + .type = READSTAT_TYPE_STRING, + .value_labels_count = 1, + .value_labels = { + { + .value = { .type = READSTAT_TYPE_STRING, .v = { .string_value = "A" } }, + .label = "Apple" + } + } + } + } + }, + + { + .label = "Too long SAS catalog format name", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS7BCAT, + .label_sets_count = 1, + .label_sets = { + { + .name = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFG", + .type = READSTAT_TYPE_DOUBLE, + .value_labels_count = 1, + .value_labels = { + { + .value = { .type = READSTAT_TYPE_DOUBLE, .v = { .double_value = 1.0 } }, + .label = "One" + } + } + } + } + }, + { .label = "DTA ancient value labels", .write_error = READSTAT_ERROR_NUMERIC_VALUE_IS_OUT_OF_RANGE, @@ -3914,7 +4033,7 @@ static rt_test_group_t _test_groups[] = { .label_sets_count = 1, .label_sets = { { - .name = "$YESNO", + .name = "$YESNO.", .type = READSTAT_TYPE_STRING, .value_labels_count = 2, .value_labels = { @@ -3943,7 +4062,7 @@ static rt_test_group_t _test_groups[] = { .label_sets_count = 1, .label_sets = { { - .name = "$YESNO", + .name = "$YESNO.", .type = READSTAT_TYPE_STRING, .value_labels_count = 2, .value_labels = {