Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions src/sas/readstat_sas7bdat_read.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
44 changes: 19 additions & 25 deletions src/sas/readstat_sas7bdat_write.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------------------------------------------- */
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/sas/readstat_xport.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
107 changes: 79 additions & 28 deletions src/test/test_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
}
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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" }
}
}
}
},
Expand Down Expand Up @@ -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 } },
Expand All @@ -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 } },
Expand All @@ -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 } },
Expand All @@ -3682,6 +3731,7 @@ static rt_test_group_t _test_groups[] = {
},
{
.name = "DateTimeHi",
.format = "DATETIME28.9",
.informat = "23.9",
.type = READSTAT_TYPE_DOUBLE,
.values = {
Expand All @@ -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 } },
Expand Down
2 changes: 1 addition & 1 deletion src/test/test_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading