static SQLRETURN parse_interval_iso8601()

in driver/convert.c [2108:2372]


static SQLRETURN parse_interval_iso8601(esodbc_rec_st *rec,
	SQLSMALLINT ctype, wstr_st *wstr, SQL_INTERVAL_STRUCT *ivl)
{
	esodbc_stmt_st *stmt = rec->desc->hdr.stmt;
	char sign;
	SQLWCHAR *crr, *end;
	wstr_st nr;
	SQLUINTEGER uint, fraction;
	BOOL has_fraction;
	enum {ST_UNINITED, ST_PERIOD, ST_TIME, ST_NUMBER} state, saved;
	uint16_t fields_bm; /* read fields bit mask */
	static const uint16_t type2bm[] = {
		1 << SQL_IS_YEAR,
			1 << SQL_IS_MONTH,
			1 << SQL_IS_DAY,
			1 << SQL_IS_HOUR,
			1 << SQL_IS_MINUTE,
			1 << SQL_IS_SECOND,
			(1 << SQL_IS_YEAR) | (1 << SQL_IS_MONTH),
			(1 << SQL_IS_DAY) | (1 << SQL_IS_HOUR),
			(1 << SQL_IS_DAY) | (1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE),
			(1 << SQL_IS_DAY) | (1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE) |
			(1 << SQL_IS_SECOND),
			(1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE),
			(1 << SQL_IS_HOUR) | (1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND),
			(1 << SQL_IS_MINUTE) | (1 << SQL_IS_SECOND),
	};
	uint16_t type2bm_ivl; /* the type bit mask for the interval */
	SQLRETURN ret;
	SQLUINTEGER secs; /* ~ond~ */

	/* Sets a bit in a bitmask corresponding to one interval field, given
	 * `_ivl`, or errs if already set.
	 * Uses local vars: fields_bm.
	 * Jumps to err_format on error. */
#	define SET_BITMASK_OR_ERR(_ivl) \
	do { \
		uint8_t _bit = 1 << (_ivl); \
		if (_bit & fields_bm) { \
			ERRH(stmt, "field %d already set.", (_ivl)); \
			goto err_format; \
		} \
		fields_bm |= _bit; \
	} while (0)
	/* Assigns a value to an interval field, `_ivl_field` of type `_ivl_type`,
	 * if current state  is ST_NUMBER and the previous one matches the given
	 * `_prev_state`.
	 * Uses local vars: ivl, fields_bm, state, saved.
	 * Jumps to err_format on error. */
#	define ASSIGN_FIELD(_prev_state, _ivl_type, _ivl_field_) \
	do { \
		if (state != ST_NUMBER || saved != _prev_state) { \
			goto err_format; \
		} \
		if (_ivl_type != SQL_IS_SECOND && has_fraction) { \
			goto err_format; \
		} \
		SET_BITMASK_OR_ERR(_ivl_type); \
		ivl->intval._ivl_field_ = uint; \
		DBGH(stmt, "field %d assigned value %lu.", _ivl_type, uint); \
		state = saved; \
	} while (0)
	/* Safe ulong addition */
#	define ULONG_SAFE_ADD(_to, _from) \
	do { \
		if (ULONG_MAX - (_from) < _to) { \
			goto err_overflow; \
		} else { \
			_to += _from; \
		} \
	} while (0)

	/* the interval type will be used as bitmask indexes */
	assert(0 < SQL_IS_YEAR && SQL_IS_MINUTE_TO_SECOND < 16);
	DBGH(stmt, "ISO 8601 to parse: [%zu] `" LWPDL "`, C target: %hd.",
		wstr->cnt, LWSTR(wstr), ctype);

	memset(ivl, 0, sizeof(*ivl));
	sign = 0;
	fields_bm = 0;
	fraction = 0;
	state = saved = ST_UNINITED;
	for (crr = wstr->str, end = wstr->str + wstr->cnt; crr < end; ) {
		switch (*crr | 0x20) { /* ~tolower(), ascii vals only */
			case L'p':
				if (state != ST_UNINITED) {
					goto err_parse;
				}
				state = ST_PERIOD;
				break;
			case L't':
				if (state != ST_PERIOD) {
					goto err_parse;
				}
				state = ST_TIME;
				break;
			case L'+':
			case L'-':
			case L'.':
			/* L'0' .. L'9' */
			case L'0':
			case L'1':
			case L'2':
			case L'3':
			case L'4':
			case L'5':
			case L'6':
			case L'7':
			case L'8':
			case L'9':
				if (state != ST_PERIOD && state != ST_TIME) {
					goto err_parse;
				}
				nr.str = crr;
				nr.cnt = end - crr;
				ret = parse_iso8601_number(rec, &nr, &uint, &sign,
						&fraction, &has_fraction);
				if (! SQL_SUCCEEDED(ret)) {
					goto err_format;
				} else {
					crr = nr.str;
				}
				saved = state;
				state = ST_NUMBER;
				continue;

			case L'y':
				ASSIGN_FIELD(ST_PERIOD, SQL_IS_YEAR, year_month.year);
				break;
			case L'm':
				if (state != ST_NUMBER ||
					(saved != ST_PERIOD && saved != ST_TIME)) {
					goto err_format;
				}
				if (has_fraction) {
					goto err_format;
				}
				if (saved == ST_PERIOD) {
					SET_BITMASK_OR_ERR(SQL_IS_MONTH);
					ivl->intval.year_month.month = uint;
					DBGH(stmt, "field %d assigned value %lu.", SQL_IS_MONTH,
						uint);
				} else {
					SET_BITMASK_OR_ERR(SQL_IS_MINUTE);
					ivl->intval.day_second.minute = uint;
					DBGH(stmt, "field %d assigned value %lu.", SQL_IS_MONTH,
						uint);
				}
				state = saved;
				break;
			case L'd':
				ASSIGN_FIELD(ST_PERIOD, SQL_IS_DAY, day_second.day);
				break;
			case L'h':
				ASSIGN_FIELD(ST_TIME, SQL_IS_HOUR, day_second.hour);
				break;
			case L's':
				ASSIGN_FIELD(ST_TIME, SQL_IS_SECOND, day_second.second);
				if (has_fraction) {
					ivl->intval.day_second.fraction = fraction;
					DBGH(stmt, "field fraction assigned value %lu.", fraction);
				}
				break;
			default:
				goto err_parse;
		}
		crr ++;
	}
	if (state != ST_PERIOD && state != ST_TIME) {
		goto err_format;
	}

	ivl->interval_sign = (sign < 0) ? SQL_TRUE : SQL_FALSE;
	assert(SQL_CODE_YEAR == SQL_IS_YEAR);
	ivl->interval_type = ctype - (SQL_INTERVAL_YEAR - SQL_CODE_YEAR);

	assert(0 < /*starts at 1*/ ivl->interval_type &&
		ivl->interval_type < 8 * sizeof(type2bm)/sizeof(type2bm[0]));

	type2bm_ivl = type2bm[ivl->interval_type - 1];
	/* If the expression set fields not directly relevant to the interval AND
	 * the interval is not of type year/-/month one (which does seem to
	 * conform to the standard), rebalance the member values as expected by
	 * the interval type. */
	if ((type2bm_ivl != fields_bm) && ((type2bm_ivl &
				((1 << SQL_CODE_YEAR) | (1 << SQL_CODE_MONTH))) == 0)) {
		secs = ivl->intval.day_second.second;
		ULONG_SAFE_ADD(secs, 60 * ivl->intval.day_second.minute); //...
		ULONG_SAFE_ADD(secs, 3600 * ivl->intval.day_second.hour);
		ULONG_SAFE_ADD(secs, 24 * 3600 * ivl->intval.day_second.day);
		/* clear everything set, but reinstate any set fractions */
		fields_bm = 0;
		memset(&ivl->intval.day_second, 0, sizeof(ivl->intval.day_second));
		ivl->intval.day_second.fraction = fraction;

		if (type2bm_ivl & (1 << SQL_CODE_SECOND)) {
			ivl->intval.day_second.second = secs;
			fields_bm |= (1 << SQL_CODE_SECOND);
		} else if (has_fraction) {
			/* fraction val itself is truncated away due to precision
			 * zero/null for intervals with no seconds component */
			ERRH(stmt, "fraction in interval with no second component");
			goto err_format;
		}
		if (type2bm_ivl & (1 << SQL_CODE_MINUTE)) {
			if (ivl->intval.day_second.second) {
				ivl->intval.day_second.minute =
					ivl->intval.day_second.second / 60;
				ivl->intval.day_second.second = secs % 60;
			} else {
				ivl->intval.day_second.minute = secs / 60;
				assert(secs % 60 == 0);
			}
			fields_bm |= (1 << SQL_CODE_MINUTE);
		}
		if (type2bm_ivl & (1 << SQL_CODE_HOUR)) {
			if (ivl->intval.day_second.minute) {
				ivl->intval.day_second.hour =
					ivl->intval.day_second.minute / 60;
				ivl->intval.day_second.minute %= 60;
			} else {
				ivl->intval.day_second.hour = secs / 3600;
				assert(secs % 3600 == 0);
			}
			fields_bm |= (1 << SQL_CODE_HOUR);
		}
		if (type2bm_ivl & (1 << SQL_CODE_DAY)) {
			if (ivl->intval.day_second.hour) {
				ivl->intval.day_second.day =
					ivl->intval.day_second.hour / 24;
				ivl->intval.day_second.hour %= 24;
			} else {
				ivl->intval.day_second.day = secs / (24 * 3600);
				assert(secs % (24 * 3600) == 0);
			}
			fields_bm |= (1 << SQL_CODE_DAY);
		}
	}

	/* Check that the ISO value has no fields set other than those allowed
	 * for the advertised type. Since the year_month and day_second form a
	 * union, this can't be done by checks against field values. */
	if (~type2bm_ivl & fields_bm) {
		ERRH(stmt, "illegal fields (0x%hx) for interval type %hd (0x%hx).",
			fields_bm, ctype, type2bm_ivl);
		goto err_format;
	}

	return ret;

err_overflow:
	ERRH(stmt, "integer overflow while normalizing ISO8601 format [%zu] `"
		LWPDL "`.", wstr->cnt, LWSTR(wstr));
	RET_HDIAGS(stmt, SQL_STATE_22015);
err_parse:
	ERRH(stmt, "unexpected current char `%c` in state %d.", *crr, state);
err_format:
	ERRH(stmt, "invalid ISO8601 format [%zu] `" LWPDL "`.", wstr->cnt,
		LWSTR(wstr));
	RET_HDIAG(stmt, SQL_STATE_22018, "Invalid server answer", 0);

#	undef ASSIGN_FIELD
#	undef SET_BITMASK_OR_ERR
#	undef ULONG_SAFE_ADD
}