Commit 19d4402788 for bind

commit 19d4402788098dee623b30822c52fa94e18f99b8
Author: Alessio Podda <alessio@isc.org>
Date:   Mon Aug 31 15:25:04 2026 +0200

    Timestamp parsing optimization

    Add a more optimized timestamp parsing algorithm as it is a bottleneck
    for rrsig text parsing.

diff --git a/lib/dns/dst_api.c b/lib/dns/dst_api.c
index 7ce316b187..439d7cd610 100644
--- a/lib/dns/dst_api.c
+++ b/lib/dns/dst_api.c
@@ -1704,7 +1704,8 @@ dst_key_read_state(const char *filename, isc_mem_t *mctx, dst_key_t **keyp) {
 				BADTOKEN();
 			}

-			CHECK(dns_time32_fromtext(DST_AS_STR(token), &when));
+			CHECK(dns_time32_fromregion(token.value.as_textregion,
+						    &when));

 			dst_key_settime(*keyp, tag, when);
 			goto next;
diff --git a/lib/dns/dst_parse.c b/lib/dns/dst_parse.c
index 8fbb00fa61..6f649d691a 100644
--- a/lib/dns/dst_parse.c
+++ b/lib/dns/dst_parse.c
@@ -533,7 +533,8 @@ dst__privstruct_parse(dst_key_t *key, unsigned int alg, isc_lex_t *lex,
 				goto cleanup;
 			}

-			CHECK(dns_time32_fromtext(DST_AS_STR(token), &when));
+			CHECK(dns_time32_fromregion(token.value.as_textregion,
+						    &when));

 			dst_key_settime(key, tag, when);

diff --git a/lib/dns/include/dns/time.h b/lib/dns/include/dns/time.h
index 866d3fcebd..9f8f422fd8 100644
--- a/lib/dns/include/dns/time.h
+++ b/lib/dns/include/dns/time.h
@@ -22,6 +22,7 @@
 #include <inttypes.h>

 #include <isc/buffer.h>
+#include <isc/region.h>

 /***
  ***	Functions
@@ -35,6 +36,13 @@ dns_time64_fromtext(const char *source, int64_t *target);
  * Store the count at 'target'.
  */

+isc_result_t
+dns_time64_fromregion(isc_textregion_t source, int64_t *target);
+/*%<
+ * Like dns_time64_fromtext, but read the text from the length-delimited
+ * region 'source'.
+ */
+
 isc_result_t
 dns_time32_fromtext(const char *source, uint32_t *target);
 /*%<
@@ -42,6 +50,13 @@ dns_time32_fromtext(const char *source, uint32_t *target);
  * as per RFC2535.
  */

+isc_result_t
+dns_time32_fromregion(isc_textregion_t source, uint32_t *target);
+/*%<
+ * Like dns_time64_fromregion, but returns the second count modulo 2^32 as per
+ * RFC2535.
+ */
+
 isc_result_t
 dns_time64_totext(int64_t value, isc_buffer_t *target);
 /*%<
diff --git a/lib/dns/master.c b/lib/dns/master.c
index 0b8db4f181..cc8b0d5a9c 100644
--- a/lib/dns/master.c
+++ b/lib/dns/master.c
@@ -1206,8 +1206,9 @@ load_text(dns_loadctx_t *lctx) {
 				isc_stdtime_t dump_time;
 				isc_stdtime_t current_time = isc_stdtime_now();
 				GETTOKEN(lctx->lex, 0, &token, false);
-				result = dns_time64_fromtext(DNS_AS_STR(token),
-							     &dump_time64);
+				result = dns_time64_fromregion(
+					token.value.as_textregion,
+					&dump_time64);
 				if (MANYERRS(lctx, result)) {
 					SETRESULT(lctx, result);
 					LOGIT(result);
diff --git a/lib/dns/rdata/generic/keydata_65533.c b/lib/dns/rdata/generic/keydata_65533.c
index ee8cf0307a..cffe24de60 100644
--- a/lib/dns/rdata/generic/keydata_65533.c
+++ b/lib/dns/rdata/generic/keydata_65533.c
@@ -40,19 +40,19 @@ fromtext_keydata(ARGS_FROMTEXT) {
 	/* refresh timer */
 	RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
 				      false));
-	RETTOK(dns_time32_fromtext(DNS_AS_STR(token), &refresh));
+	RETTOK(dns_time32_fromregion(token.value.as_textregion, &refresh));
 	RETERR(uint32_tobuffer(refresh, target));

 	/* add hold-down */
 	RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
 				      false));
-	RETTOK(dns_time32_fromtext(DNS_AS_STR(token), &addhd));
+	RETTOK(dns_time32_fromregion(token.value.as_textregion, &addhd));
 	RETERR(uint32_tobuffer(addhd, target));

 	/* remove hold-down */
 	RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
 				      false));
-	RETTOK(dns_time32_fromtext(DNS_AS_STR(token), &removehd));
+	RETTOK(dns_time32_fromregion(token.value.as_textregion, &removehd));
 	RETERR(uint32_tobuffer(removehd, target));

 	/* flags */
diff --git a/lib/dns/rdata/generic/rrsig_46.c b/lib/dns/rdata/generic/rrsig_46.c
index a4f9b4916b..11067ae672 100644
--- a/lib/dns/rdata/generic/rrsig_46.c
+++ b/lib/dns/rdata/generic/rrsig_46.c
@@ -89,8 +89,8 @@ fromtext_rrsig(ARGS_FROMTEXT) {
 	 */
 	RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
 				      false));
-	if (strlen(DNS_AS_STR(token)) <= 10U && *DNS_AS_STR(token) != '-' &&
-	    *DNS_AS_STR(token) != '+')
+	if (token.value.as_textregion.length <= 10U &&
+	    *DNS_AS_STR(token) != '-' && *DNS_AS_STR(token) != '+')
 	{
 		char *end;
 		unsigned long u;
@@ -105,7 +105,8 @@ fromtext_rrsig(ARGS_FROMTEXT) {
 		}
 		time_expire = u;
 	} else {
-		RETTOK(dns_time32_fromtext(DNS_AS_STR(token), &time_expire));
+		RETTOK(dns_time32_fromregion(token.value.as_textregion,
+					     &time_expire));
 	}
 	RETERR(uint32_tobuffer(time_expire, target));

@@ -114,8 +115,8 @@ fromtext_rrsig(ARGS_FROMTEXT) {
 	 */
 	RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
 				      false));
-	if (strlen(DNS_AS_STR(token)) <= 10U && *DNS_AS_STR(token) != '-' &&
-	    *DNS_AS_STR(token) != '+')
+	if (token.value.as_textregion.length <= 10U &&
+	    *DNS_AS_STR(token) != '-' && *DNS_AS_STR(token) != '+')
 	{
 		char *end;
 		unsigned long u;
@@ -130,7 +131,8 @@ fromtext_rrsig(ARGS_FROMTEXT) {
 		}
 		time_signed = u;
 	} else {
-		RETTOK(dns_time32_fromtext(DNS_AS_STR(token), &time_signed));
+		RETTOK(dns_time32_fromregion(token.value.as_textregion,
+					     &time_signed));
 	}
 	RETERR(uint32_tobuffer(time_signed, target));

diff --git a/lib/dns/rdata/generic/sig_24.c b/lib/dns/rdata/generic/sig_24.c
index 35cede14d8..71cde4686b 100644
--- a/lib/dns/rdata/generic/sig_24.c
+++ b/lib/dns/rdata/generic/sig_24.c
@@ -85,7 +85,7 @@ fromtext_sig(ARGS_FROMTEXT) {
 	 */
 	RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
 				      false));
-	RETTOK(dns_time32_fromtext(DNS_AS_STR(token), &time_expire));
+	RETTOK(dns_time32_fromregion(token.value.as_textregion, &time_expire));
 	RETERR(uint32_tobuffer(time_expire, target));

 	/*
@@ -93,7 +93,7 @@ fromtext_sig(ARGS_FROMTEXT) {
 	 */
 	RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_string,
 				      false));
-	RETTOK(dns_time32_fromtext(DNS_AS_STR(token), &time_signed));
+	RETTOK(dns_time32_fromregion(token.value.as_textregion, &time_signed));
 	RETERR(uint32_tobuffer(time_signed, target));

 	/*
diff --git a/lib/dns/skr.c b/lib/dns/skr.c
index 77b2bed723..cf057d27cb 100644
--- a/lib/dns/skr.c
+++ b/lib/dns/skr.c
@@ -286,7 +286,8 @@ dns_skr_read(isc_mem_t *mctx, const char *filename, dns_name_t *origin,
 			}

 			/* Create new bundle */
-			CHECK(dns_time32_fromtext(STR(token), &bundle_id));
+			CHECK(dns_time32_fromregion(token.value.as_textregion,
+						    &bundle_id));
 			bundle = NULL;
 			skrbundle_create(mctx, (isc_stdtime_t)bundle_id,
 					 &bundle);
diff --git a/lib/dns/time.c b/lib/dns/time.c
index 5520ae9df8..574f137d12 100644
--- a/lib/dns/time.c
+++ b/lib/dns/time.c
@@ -13,7 +13,6 @@

 /*! \file */

-#include <ctype.h>
 #include <inttypes.h>
 #include <stdio.h>
 #include <time.h>
@@ -29,6 +28,31 @@

 static const int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

+static int64_t
+date_to_days(uint32_t year, uint32_t month, uint32_t day) {
+	/*
+	 * This is the Neri-Schneider calendar algorithm from "Euclidean affine
+	 * functions and their application to calendar algorithms".
+	 *
+	 * Map January and February to the end of the preceding year.  Shift
+	 * the calendar by one 400-year cycle so that dates in year zero can
+	 * still be calculated using unsigned arithmetic.
+	 */
+	const uint32_t jan_feb = month <= 2U;
+	const uint32_t y = year + 400U - jan_feb;
+	const uint32_t m = month + 12U * jan_feb;
+	const uint32_t century = y / 100U;
+	const uint32_t days_to_year = 1461U * y / 4U - century + century / 4U;
+	const uint32_t days_to_month = (979U * m - 2919U) / 32U;
+	const uint32_t shifted_days = days_to_year + days_to_month + day - 1U;
+
+	/*
+	 * 719468 selects the Unix epoch; 146097 compensates for the 400-year
+	 * shift above.
+	 */
+	return (int64_t)shifted_days - (719468LL + 146097LL);
+}
+
 isc_result_t
 dns_time64_totext(int64_t t, isc_buffer_t *target) {
 	struct tm tm;
@@ -127,70 +151,70 @@ dns_time32_totext(uint32_t value, isc_buffer_t *target) {
 	return dns_time64_totext(dns_time64_from32(value), target);
 }

-isc_result_t
-dns_time64_fromtext(const char *source, int64_t *target) {
-	int year, month, day, hour, minute, second;
-	int64_t value;
-	int secs;
-	int i;
-
-#define RANGE(min, max, value)                      \
-	do {                                        \
-		if (value < (min) || value > (max)) \
-			return ((ISC_R_RANGE));     \
-	} while (0)
+static isc_result_t
+time64_fromtext(const char *source, size_t length, int64_t *target) {
+	uint32_t digits[14];
+	uint32_t year, month, day, hour, minute, second;
+	uint32_t month_days;
+	unsigned int i;

-	if (strlen(source) != 14U) {
+	if (length != ARRAY_SIZE(digits)) {
 		return DNS_R_SYNTAX;
 	}
-	/*
-	 * Confirm the source only consists digits.  sscanf() allows some
-	 * minor exceptions.
-	 */
-	for (i = 0; i < 14; i++) {
-		if (!isdigit((unsigned char)source[i])) {
+	for (i = 0; i < ARRAY_SIZE(digits); i++) {
+		digits[i] = (unsigned char)source[i] - (unsigned int)'0';
+		if (digits[i] > 9U) {
 			return DNS_R_SYNTAX;
 		}
 	}
-	if (sscanf(source, "%4d%2d%2d%2d%2d%2d", &year, &month, &day, &hour,
-		   &minute, &second) != 6)
+
+	/* "YYYYxxxxxxxxxx" */
+	year = digits[0] * 1000U + digits[1] * 100U + digits[2] * 10U +
+	       digits[3];
+	/* "xxxxMMxxxxxxxx" */
+	month = digits[4] * 10U + digits[5];
+	/* "xxxxxxDDxxxxxx" */
+	day = digits[6] * 10U + digits[7];
+	/* "xxxxxxxxHHxxxx" */
+	hour = digits[8] * 10U + digits[9];
+	/* "xxxxxxxxxxMMxx" */
+	minute = digits[10] * 10U + digits[11];
+	/* "xxxxxxxxxxxxSS" */
+	second = digits[12] * 10U + digits[13];
+
+	if (month - 1U >= ARRAY_SIZE(days)) {
+		return ISC_R_RANGE;
+	}
+	month_days = days[month - 1U] +
+		     ((month == 2U && is_leap(year)) ? 1U : 0U);
+	if (day - 1U >= month_days || hour > 23U || minute > 59U ||
+	    second > 60U) /* 60 == leap second. */
 	{
-		return DNS_R_SYNTAX;
+		return ISC_R_RANGE;
 	}

-	RANGE(0, 9999, year);
-	RANGE(1, 12, month);
-	RANGE(1, days[month - 1] + ((month == 2 && is_leap(year)) ? 1 : 0),
-	      day);
+	*target = date_to_days(year, month, day) * 86400LL + hour * 3600U +
+		  minute * 60U + second;
+	return ISC_R_SUCCESS;
+}
+
+isc_result_t
+dns_time64_fromregion(isc_textregion_t source, int64_t *target) {
+	return time64_fromtext(source.base, source.length, target);
+}

-	RANGE(0, 23, hour);
-	RANGE(0, 59, minute);
-	RANGE(0, 60, second); /* 60 == leap second. */
+isc_result_t
+dns_time64_fromtext(const char *source, int64_t *target) {
+	return time64_fromtext(source, strlen(source), target);
+}

-	/*
-	 * Calculate seconds from epoch.
-	 * Note: this uses a idealized calendar.
-	 */
-	value = second + (60 * minute) + (3600 * hour) + ((day - 1) * 86400);
-	for (i = 0; i < (month - 1); i++) {
-		value += days[i] * 86400;
-	}
-	if (is_leap(year) && month > 2) {
-		value += 86400;
-	}
-	if (year < 1970) {
-		for (i = 1969; i >= year; i--) {
-			secs = (is_leap(i) ? 366 : 365) * 86400;
-			value -= secs;
-		}
-	} else {
-		for (i = 1970; i < year; i++) {
-			secs = (is_leap(i) ? 366 : 365) * 86400;
-			value += secs;
-		}
-	}
+isc_result_t
+dns_time32_fromregion(isc_textregion_t source, uint32_t *target) {
+	int64_t value64;
+
+	RETERR(dns_time64_fromregion(source, &value64));
+	*target = (uint32_t)value64;

-	*target = value;
 	return ISC_R_SUCCESS;
 }

diff --git a/lib/dns/view.c b/lib/dns/view.c
index c82d3358be..22c28f00ff 100644
--- a/lib/dns/view.c
+++ b/lib/dns/view.c
@@ -1706,7 +1706,7 @@ dns_view_loadnta(dns_view_t *view) {

 	for (;;) {
 		int options = (ISC_LEXOPT_EOL | ISC_LEXOPT_EOF);
-		char *name, *type, *timestamp;
+		char *name, *type;
 		size_t len;
 		dns_fixedname_t fn;
 		const dns_name_t *ntaname;
@@ -1753,8 +1753,7 @@ dns_view_loadnta(dns_view_t *view) {
 		if (token.type != isc_tokentype_string) {
 			CLEANUP(ISC_R_UNEXPECTEDTOKEN);
 		}
-		timestamp = TSTR(token);
-		CHECK(dns_time32_fromtext(timestamp, &t));
+		CHECK(dns_time32_fromregion(token.value.as_textregion, &t));

 		CHECK(isc_lex_gettoken(lex, options, &token));
 		if (token.type != isc_tokentype_eol &&
diff --git a/tests/dns/time_test.c b/tests/dns/time_test.c
index 8002c0ed21..2a383e01a8 100644
--- a/tests/dns/time_test.c
+++ b/tests/dns/time_test.c
@@ -159,6 +159,65 @@ ISC_RUN_TEST_IMPL(some_ago) {
 	assert_int_equal(when, test_time);
 }

+ISC_RUN_TEST_IMPL(fromregion) {
+	char text[] = "x19700101000000y";
+	isc_textregion_t source = {
+		.base = &text[1],
+		.length = 14,
+	};
+	int64_t when;
+
+	UNUSED(state);
+
+	assert_int_equal(dns_time64_fromregion(source, &when), ISC_R_SUCCESS);
+	assert_int_equal(when, 0);
+}
+
+ISC_RUN_TEST_IMPL(fromtext_boundaries) {
+	static const struct {
+		const char *text;
+		int64_t when;
+	} tests[] = {
+		{ "00000101000000", -62167219200LL },
+		{ "19691231235959", -1 },
+		{ "19700101000000", 0 },
+		{ "99991231235959", 253402300799LL },
+	};
+	int64_t when;
+
+	UNUSED(state);
+
+	for (size_t i = 0; i < ARRAY_SIZE(tests); i++) {
+		assert_int_equal(dns_time64_fromtext(tests[i].text, &when),
+				 ISC_R_SUCCESS);
+		assert_int_equal(when, tests[i].when);
+	}
+}
+
+ISC_RUN_TEST_IMPL(fromtext_invalid) {
+	static const char *syntax[] = {
+		"1970010100000",
+		"197001010000000",
+		"1970010100000x",
+	};
+	static const char *range[] = {
+		"19700001000000", "19701301000000", "19700229000000",
+		"19700101240000", "19700101006000", "19700101000061",
+	};
+	int64_t when;
+
+	UNUSED(state);
+
+	for (size_t i = 0; i < ARRAY_SIZE(syntax); i++) {
+		assert_int_equal(dns_time64_fromtext(syntax[i], &when),
+				 DNS_R_SYNTAX);
+	}
+	for (size_t i = 0; i < ARRAY_SIZE(range); i++) {
+		assert_int_equal(dns_time64_fromtext(range[i], &when),
+				 ISC_R_RANGE);
+	}
+}
+
 ISC_TEST_LIST_START
 ISC_TEST_ENTRY(epoch_minus_one)
 ISC_TEST_ENTRY(epoch)
@@ -166,6 +225,9 @@ ISC_TEST_ENTRY(half_maxint)
 ISC_TEST_ENTRY(half_plus_one)
 ISC_TEST_ENTRY(fifty_before)
 ISC_TEST_ENTRY(some_ago)
+ISC_TEST_ENTRY(fromregion)
+ISC_TEST_ENTRY(fromtext_boundaries)
+ISC_TEST_ENTRY(fromtext_invalid)
 ISC_TEST_LIST_END

 ISC_TEST_MAIN