diff --git a/src/sas/readstat_sas7bdat_read.c b/src/sas/readstat_sas7bdat_read.c index 8b804353..3bf11581 100644 --- a/src/sas/readstat_sas7bdat_read.c +++ b/src/sas/readstat_sas7bdat_read.c @@ -746,14 +746,36 @@ static readstat_error_t sas7bdat_construct_format(char *dst, size_t dst_len, if (retval != READSTAT_OK) return retval; - size_t len = strlen(dst); - if (width && len < dst_len - 1) { - snprintf(dst + len, dst_len - len, "%d", width); - len = strlen(dst); + /* Digits require a width */ + if (width == 0 && digits != 0) + return READSTAT_ERROR_PARSE; + + if (strchr(dst, '.')) { + /* Legacy layout: earlier ReadStat writers stored the full format + * string (e.g. "YYMMDD10." or "BEST.") verbatim as the name, with + * width and digits both 0. Return it unchanged. */ + if (width == 0 && digits == 0) + return READSTAT_OK; + return READSTAT_ERROR_PARSE; } - if (len && digits && len < dst_len - 1) { - snprintf(dst + len, dst_len - len, ".%d", digits); + + /* No format at all */ + if (dst[0] == '\0' && width == 0) + return READSTAT_OK; + + /* NAME. / NAMEw. / NAMEw.d (NAME may be empty, as in the w.d format) */ + size_t len = strlen(dst); + int written; + if (digits) { + written = snprintf(dst + len, dst_len - len, "%d.%d", width, digits); + } else if (width) { + written = snprintf(dst + len, dst_len - len, "%d.", width); + } else { + written = snprintf(dst + len, dst_len - len, "."); } + if (written < 0 || (size_t)written >= dst_len - len) + return READSTAT_ERROR_PARSE; + return READSTAT_OK; } diff --git a/src/sas/readstat_sas7bdat_write.c b/src/sas/readstat_sas7bdat_write.c index f5e8c011..943dc373 100644 --- a/src/sas/readstat_sas7bdat_write.c +++ b/src/sas/readstat_sas7bdat_write.c @@ -366,34 +366,26 @@ static readstat_error_t sas7bdat_text_add(sas7bdat_write_ctx_t *ctx, const char /* Formats -------------------------------------------------------------- */ /* Splits a format such as "DATETIME19.2" into name "DATETIME", width 19 and - * 2 digits, the way SAS stores it. If the string can't be split without - * losing information, it is stored verbatim as the format name. */ -static void sas7bdat_split_format(const char *format, char *name, size_t name_len, + * 2 digits, the way SAS stores it. */ +static readstat_error_t sas7bdat_split_format(const char *format, char *name, size_t name_len, uint16_t *width, uint16_t *digits) { name[0] = '\0'; *width = 0; *digits = 0; if (format == NULL || format[0] == '\0') - return; + return READSTAT_OK; xport_format_t parsed; - if (xport_parse_format(format, strlen(format), &parsed, NULL, NULL) == READSTAT_OK && - parsed.width >= 0 && parsed.width <= UINT16_MAX && - parsed.decimals >= 0 && parsed.decimals <= UINT16_MAX) { - char rebuilt[64]; - size_t len = snprintf(rebuilt, sizeof(rebuilt), "%s", parsed.name); - if (parsed.width && len < sizeof(rebuilt)) - len += snprintf(rebuilt + len, sizeof(rebuilt) - len, "%d", parsed.width); - if (parsed.decimals && len < sizeof(rebuilt)) - len += snprintf(rebuilt + len, sizeof(rebuilt) - len, ".%d", parsed.decimals); - if (len < sizeof(rebuilt) && strcmp(rebuilt, format) == 0) { - snprintf(name, name_len, "%s", parsed.name); - *width = parsed.width; - *digits = parsed.decimals; - return; - } - } - snprintf(name, name_len, "%s", format); + if (xport_parse_format(format, strlen(format), &parsed, NULL, NULL) != READSTAT_OK || + parsed.width < 0 || parsed.width > UINT16_MAX || + parsed.decimals < 0 || parsed.decimals > UINT16_MAX || + (parsed.width == 0 && parsed.decimals != 0)) + return READSTAT_ERROR_BAD_FORMAT_STRING; + + snprintf(name, name_len, "%s", parsed.name); + *width = parsed.width; + *digits = parsed.decimals; + return READSTAT_OK; } /* Column name hash table ----------------------------------------------- */ @@ -528,10 +520,12 @@ static readstat_error_t sas7bdat_add_column_text(readstat_writer_t *writer, sas7 size_t name_len = strlen(variable->name); size_t label_len = strlen(variable->label); - sas7bdat_split_format(variable->format, format_name, sizeof(format_name), - &ctx->format_widths[i], &ctx->format_digits[i]); - sas7bdat_split_format(variable->informat, informat_name, sizeof(informat_name), - &ctx->informat_widths[i], &ctx->informat_digits[i]); + if ((retval = sas7bdat_split_format(variable->format, format_name, sizeof(format_name), + &ctx->format_widths[i], &ctx->format_digits[i])) != READSTAT_OK) + goto cleanup; + if ((retval = sas7bdat_split_format(variable->informat, informat_name, sizeof(informat_name), + &ctx->informat_widths[i], &ctx->informat_digits[i])) != READSTAT_OK) + goto cleanup; if ((retval = sas7bdat_text_add(ctx, variable->name, &ctx->name_refs[i])) != READSTAT_OK) goto cleanup; diff --git a/src/sas/readstat_xport.h b/src/sas/readstat_xport.h index 2a2e3f7d..c3e2c7c3 100644 --- a/src/sas/readstat_xport.h +++ b/src/sas/readstat_xport.h @@ -36,7 +36,7 @@ typedef struct xport_namestr_s { #pragma pack(pop) typedef struct xport_format_s { - char name[32]; + char name[33]; int width; int decimals; } xport_format_t; diff --git a/src/test/test_list.h b/src/test/test_list.h index b24944c4..902936ff 100644 --- a/src/test/test_list.h +++ b/src/test/test_list.h @@ -812,26 +812,26 @@ static rt_test_group_t _test_groups[] = { .test_formats = RT_FORMAT_SAS7BDAT, .rows = 0, .columns = { - { .name = "one_of_many_long_column_names_1", .label = "Label for column 1", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_2", .label = "Label for column 2", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_3", .label = "Label for column 3", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_4", .label = "Label for column 4", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_5", .label = "Label for column 5", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_6", .label = "Label for column 6", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_7", .label = "Label for column 7", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_8", .label = "Label for column 8", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_9", .label = "Label for column 9", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_10", .label = "Label for column 10", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_11", .label = "Label for column 11", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_12", .label = "Label for column 12", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_13", .label = "Label for column 13", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_14", .label = "Label for column 14", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_15", .label = "Label for column 15", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_16", .label = "Label for column 16", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_17", .label = "Label for column 17", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_18", .label = "Label for column 18", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_19", .label = "Label for column 19", .format = "$CHAR8", .type = READSTAT_TYPE_STRING }, - { .name = "one_of_many_long_column_names_20", .label = "Label for column 20", .format = "$CHAR8", .type = READSTAT_TYPE_STRING } + { .name = "one_of_many_long_column_names_1", .label = "Label for column 1", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_2", .label = "Label for column 2", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_3", .label = "Label for column 3", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_4", .label = "Label for column 4", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_5", .label = "Label for column 5", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_6", .label = "Label for column 6", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_7", .label = "Label for column 7", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_8", .label = "Label for column 8", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_9", .label = "Label for column 9", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_10", .label = "Label for column 10", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_11", .label = "Label for column 11", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_12", .label = "Label for column 12", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_13", .label = "Label for column 13", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_14", .label = "Label for column 14", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_15", .label = "Label for column 15", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_16", .label = "Label for column 16", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_17", .label = "Label for column 17", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_18", .label = "Label for column 18", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_19", .label = "Label for column 19", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING }, + { .name = "one_of_many_long_column_names_20", .label = "Label for column 20", .format = "$CHAR8.", .type = READSTAT_TYPE_STRING } } } } @@ -1482,6 +1482,22 @@ static rt_test_group_t _test_groups[] = { { .name = "VAR2", .type = READSTAT_TYPE_STRING, .format = "$CHAR3.", .label_set = "$CHAR3." } } }, + { + .label = "SAS7BDAT format without width", + .test_formats = RT_FORMAT_SAS7BDAT, + .columns = { + { .name = "VAR1", .type = READSTAT_TYPE_DOUBLE, .format = "DOLLAR.", .informat = "BEST." }, + { .name = "VAR2", .type = READSTAT_TYPE_STRING, .format = "$UPCASE.", .informat = "$CHAR." }, + } + }, + { + .label = "SAS 32-character format names", + .test_formats = RT_FORMAT_SAS7BDAT | RT_FORMAT_XPORT_8, + .columns = { + { .name = "VAR1", .type = READSTAT_TYPE_DOUBLE, .format = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEF12.2" }, + { .name = "VAR2", .type = READSTAT_TYPE_STRING, .format = "$ABCDEFGHIJKLMNOPQRSTUVWXYZABCDE8." }, + } + }, { .label = "SAS informats", .test_formats = RT_FORMAT_SAS7BDAT | RT_FORMAT_XPORT_8, @@ -1509,14 +1525,44 @@ static rt_test_group_t _test_groups[] = { } }, { - .label = "XPT bad formats", + .label = "SAS format with two dots", .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, - .test_formats = RT_FORMAT_XPORT_5 | RT_FORMAT_XPORT_8, + .test_formats = RT_FORMAT_SAS, + .columns = { + { .name = "VAR1", .type = READSTAT_TYPE_DOUBLE, .format = "10.3.", .label_set = "10.3." } + } + }, + { + .label = "SAS character format with digits", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS, + .columns = { + { .name = "VAR2", .type = READSTAT_TYPE_STRING, .format = "$CHAR3.1", .label_set = "$CHAR3.1" } + } + }, + { + .label = "SAS format with negative digits", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS, .columns = { - { .name = "VAR1", .type = READSTAT_TYPE_DOUBLE, .format = "10.3.", .label_set = "10.3." }, - { .name = "VAR2", .type = READSTAT_TYPE_STRING, .format = "$CHAR3.1", .label_set = "$CHAR3.1" }, { .name = "VAR3", .type = READSTAT_TYPE_DOUBLE, .format = "YYQ-1", .label_set = "YYQ-1" } } + }, + { + .label = "SAS7BDAT format with digits but no width", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS7BDAT, + .columns = { + { .name = "VAR1", .type = READSTAT_TYPE_DOUBLE, .format = ".2", .label_set = ".2" } + } + }, + { + .label = "Too long SAS7BDAT format name", + .write_error = READSTAT_ERROR_BAD_FORMAT_STRING, + .test_formats = RT_FORMAT_SAS7BDAT, + .columns = { + { .name = "VAR1", .type = READSTAT_TYPE_DOUBLE, .format = "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFG12.2" } + } } } }, @@ -3652,7 +3698,8 @@ static rt_test_group_t _test_groups[] = { .columns = { { .name = "Date1", - .informat = "F8", + .format = "YYMMDD10.", + .informat = "F8.", .type = READSTAT_TYPE_DOUBLE, .values = { { .type = READSTAT_TYPE_DOUBLE, .v = { .double_value = 5 } }, @@ -3662,7 +3709,8 @@ static rt_test_group_t _test_groups[] = { }, { .name = "Date2", - .informat = "F8", + .format = "DATE7.", + .informat = "F8.", .type = READSTAT_TYPE_DOUBLE, .values = { { .type = READSTAT_TYPE_DOUBLE, .v = { .double_value = 3 } }, @@ -3672,7 +3720,8 @@ static rt_test_group_t _test_groups[] = { }, { .name = "DateTime", - .informat = "DATETIME19", + .format = "DATETIME19.", + .informat = "DATETIME19.", .type = READSTAT_TYPE_DOUBLE, .values = { { .type = READSTAT_TYPE_DOUBLE, .v = { .double_value = -8907752836 } }, @@ -3682,6 +3731,7 @@ static rt_test_group_t _test_groups[] = { }, { .name = "DateTimeHi", + .format = "DATETIME28.9", .informat = "23.9", .type = READSTAT_TYPE_DOUBLE, .values = { @@ -3692,7 +3742,8 @@ static rt_test_group_t _test_groups[] = { }, { .name = "Taiw", - .informat = "F8", + .format = "MINGUO10.", + .informat = "F8.", .type = READSTAT_TYPE_DOUBLE, .values = { { .type = READSTAT_TYPE_DOUBLE, .v = { .double_value = -17532 } }, diff --git a/src/test/test_types.h b/src/test/test_types.h index 7cd2af83..d41311e3 100644 --- a/src/test/test_types.h +++ b/src/test/test_types.h @@ -9,7 +9,7 @@ #define RT_MAX_VALUE_LABELS 4 #define RT_MAX_STRING 96 #define RT_MAX_VALUE_LABEL_STRING 121 -#define MAX_TESTS_PER_GROUP 20 +#define MAX_TESTS_PER_GROUP 22 typedef struct rt_label_set_s {