@@ -34,17 +34,31 @@ bool tft_ver_gte(const std::string &version, const std::string &min_version) {
3434 int min_maj = 0 , min_min = 0 ;
3535
3636 // Parse both version strings — accept "major.minor" or bare "major".
37- // sscanf returns the number of fields successfully parsed; at least 1
38- // is required. A bare integer (e.g. "16") yields 1 field with minor
39- // defaulting to 0. An empty or non-numeric string yields 0, triggering
40- // the < 1 guard below. NOLINT suppresses cert-err34-c; malformed input
41- // is handled by the field count check.
37+ // After parsing, reject trailing garbage by checking that sscanf consumed
38+ // the entire string: re-format what was parsed and compare against input.
39+ // This rejects "16.", "16x", "16.1.2", etc.
40+ // NOLINT suppresses cert-err34-c; structural validation is done below.
4241 const int ver_fields = sscanf (version.c_str (), " %d.%d" , &ver_maj, &ver_min); // NOLINT(cert-err34-c)
4342 const int min_fields = sscanf (min_version.c_str (), " %d.%d" , &min_maj, &min_min); // NOLINT(cert-err34-c)
4443
4544 if (ver_fields < 1 || min_fields < 1 )
4645 return false ; // Fail conservatively on empty or unparseable input
4746
47+ // Reject partial matches by reconstructing the parsed value and comparing
48+ // it against the original string — catches "16.", "16x", "16.1.2", etc.
49+ char ver_buf[16 ], min_buf[16 ];
50+ if (ver_fields == 1 )
51+ snprintf (ver_buf, sizeof (ver_buf), " %d" , ver_maj);
52+ else
53+ snprintf (ver_buf, sizeof (ver_buf), " %d.%d" , ver_maj, ver_min);
54+ if (min_fields == 1 )
55+ snprintf (min_buf, sizeof (min_buf), " %d" , min_maj);
56+ else
57+ snprintf (min_buf, sizeof (min_buf), " %d.%d" , min_maj, min_min);
58+
59+ if (version != ver_buf || min_version != min_buf)
60+ return false ; // Trailing garbage or extra segments detected
61+
4862 // Compare major first, then minor.
4963 // Returns false conservatively on any older segment.
5064 if (ver_maj != min_maj)
0 commit comments