--- ./tests/handlers.c.orig 2026-03-17 12:27:57.000000000 -0500 +++ ./tests/handlers.c 2026-05-11 15:47:02.809237869 -0500 @@ -137,7 +137,7 @@ fail("ID does not have the correct name"); return; } - for (i = 0; i < info->attr_count; i++) { + for (i = 0; i < info->attr_count + info->default_attr_count; i++) { attr = info->attributes; while (attr->name != NULL) { if (! xcstrcmp(atts[0], attr->name)) @@ -155,6 +155,9 @@ /* Remember, two entries in atts per attribute (see above) */ atts += 2; } + + // Self-test that the test case's list of expected attributes is complete + assert_true(atts[0] == NULL); } void XMLCALL --- ./tests/handlers.h.orig 2026-03-17 12:27:57.000000000 -0500 +++ ./tests/handlers.h 2026-05-11 15:47:02.807479260 -0500 @@ -88,6 +88,7 @@ typedef struct elementInfo { const XML_Char *name; int attr_count; + int default_attr_count; const XML_Char *id_name; AttrInfo *attributes; } ElementInfo; --- ./tests/basic_tests.c.orig 2026-03-17 12:28:19.000000000 -0500 +++ ./tests/basic_tests.c 2026-05-11 15:47:02.808691972 -0500 @@ -2466,11 +2466,9 @@ {XCS("id"), XCS("one")}, {NULL, NULL}}; AttrInfo tag_info[] = {{XCS("c"), XCS("3")}, {NULL, NULL}}; - ElementInfo info[] = {{XCS("doc"), 3, XCS("id"), NULL}, - {XCS("tag"), 1, NULL, NULL}, - {NULL, 0, NULL, NULL}}; - info[0].attributes = doc_info; - info[1].attributes = tag_info; + ElementInfo info[] = {{XCS("doc"), 3, 0, XCS("id"), doc_info}, + {XCS("tag"), 1, 0, NULL, tag_info}, + {NULL, 0, 0, NULL, NULL}}; XML_Parser parser = XML_ParserCreate(NULL); assert_true(parser != NULL); @@ -2489,6 +2487,279 @@ } END_TEST +START_TEST(test_duplicate_cdata_attribute) { + /* + https://www.w3.org/TR/xml/#attdecls + + Test the following statement from the linked specification: + When more than one definition is provided for the same attribute of a given + element type, the first declaration is binding and later declarations are + ignored. + */ + + const char *text + = "\n" + "]>\n" + "\n"; + AttrInfo doc_info[] = {{XCS("attribute"), XCS("expected")}, {NULL, NULL}}; + ElementInfo info[] + = {{XCS("doc"), 0, 1, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}}; + + XML_Parser parser = XML_ParserCreate(NULL); + assert_true(parser != NULL); + + ParserAndElementInfo parserAndElementInfos = { + parser, + info, + }; + + XML_SetStartElementHandler(parser, counting_start_element_handler); + XML_SetUserData(parser, &parserAndElementInfos); + + if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE) + != XML_STATUS_OK) + xml_failure(parser); + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_duplicate_id_attribute_1) { + /* + https://www.w3.org/TR/xml/#attdecls + + Test the following statement from the linked specification: + When more than one definition is provided for the same attribute of a given + element type, the first declaration is binding and later declarations are + ignored. + */ + + const char *text + = "\n" + "]>\n" + "\n"; + AttrInfo doc_info[] = {{XCS("identifier"), XCS("expected")}, {NULL, NULL}}; + ElementInfo info[] + = {{XCS("doc"), 0, 1, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}}; + + XML_Parser parser = XML_ParserCreate(NULL); + assert_true(parser != NULL); + + ParserAndElementInfo parserAndElementInfos = { + parser, + info, + }; + + XML_SetStartElementHandler(parser, counting_start_element_handler); + XML_SetUserData(parser, &parserAndElementInfos); + + if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE) + != XML_STATUS_OK) + xml_failure(parser); + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_duplicate_id_attribute_2) { + /* + https://www.w3.org/TR/xml/#attdecls + + Test the following statement from the linked specification: + When more than one definition is provided for the same attribute of a given + element type, the first declaration is binding and later declarations are + ignored. + */ + + const char *text + = "\n" + "]>\n" + "\n"; + AttrInfo doc_info[] = {{NULL, NULL}}; + + ElementInfo info[] + = {{XCS("doc"), 0, 0, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}}; + + XML_Parser parser = XML_ParserCreate(NULL); + assert_true(parser != NULL); + + ParserAndElementInfo parserAndElementInfos = { + parser, + info, + }; + + XML_SetStartElementHandler(parser, counting_start_element_handler); + XML_SetUserData(parser, &parserAndElementInfos); + + if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE) + != XML_STATUS_OK) + xml_failure(parser); + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_duplicate_cdata_attribute_multiple_attlistdecl) { + /* + https://www.w3.org/TR/xml/#attdecls + + Test the following statement from the linked specification: + When more than one AttlistDecl is provided for a given element type, + the contents of all those provided are merged. + */ + const char *text = "\n" + " \n" + "]>\n" + "\n"; + AttrInfo doc_info[] = {{XCS("attribute"), XCS("expected")}, {NULL, NULL}}; + ElementInfo info[] + = {{XCS("doc"), 0, 1, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}}; + + XML_Parser parser = XML_ParserCreate(NULL); + assert_true(parser != NULL); + + ParserAndElementInfo parserAndElementInfos = { + parser, + info, + }; + + XML_SetStartElementHandler(parser, counting_start_element_handler); + XML_SetUserData(parser, &parserAndElementInfos); + + if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE) + != XML_STATUS_OK) + xml_failure(parser); + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_duplicate_cdata_attribute_multiple_attlistdecl_2) { + /* + https://www.w3.org/TR/xml/#attdecls + + Test the following statement from the linked specification: + When more than one AttlistDecl is provided for a given element type, + the contents of all those provided are merged. + */ + const char *text = "\n" + " \n" + " \n" + "]>\n" + "\n"; + AttrInfo doc_info[] = {{XCS("attribute"), XCS("expected_doc")}, {NULL, NULL}}; + AttrInfo tag_info[] = {{XCS("attribute"), XCS("expected_tag")}, {NULL, NULL}}; + ElementInfo info[] = {{XCS("doc"), 0, 1, NULL, doc_info}, + {XCS("tag"), 0, 1, NULL, tag_info}, + {NULL, 0, 0, NULL, NULL}}; + + XML_Parser parser = XML_ParserCreate(NULL); + assert_true(parser != NULL); + + ParserAndElementInfo parserAndElementInfos = { + parser, + info, + }; + + XML_SetStartElementHandler(parser, counting_start_element_handler); + XML_SetUserData(parser, &parserAndElementInfos); + + if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE) + != XML_STATUS_OK) + xml_failure(parser); + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_duplicate_cdata_attribute_multiple_attlistdecl_3) { + /* + https://www.w3.org/TR/xml/#attdecls + + Test the following statement from the linked specification: + When more than one AttlistDecl is provided for a given element type, + the contents of all those provided are merged. + */ + const char *text + = "\n" + " \n" + " \n" + "]>\n" + "\n"; + AttrInfo doc_info[] = {{XCS("attribute"), XCS("expected_doc")}, + {XCS("second_attribute"), XCS("second_expected_doc")}, + {NULL, NULL}}; + AttrInfo tag_info[] = {{XCS("attribute"), XCS("expected_tag")}, {NULL, NULL}}; + ElementInfo info[] = {{XCS("doc"), 0, 2, NULL, doc_info}, + {XCS("tag"), 0, 1, NULL, tag_info}, + {NULL, 0, 0, NULL, NULL}}; + + XML_Parser parser = XML_ParserCreate(NULL); + assert_true(parser != NULL); + + ParserAndElementInfo parserAndElementInfos = { + parser, + info, + }; + + XML_SetStartElementHandler(parser, counting_start_element_handler); + XML_SetUserData(parser, &parserAndElementInfos); + + if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE) + != XML_STATUS_OK) + xml_failure(parser); + + XML_ParserFree(parser); +} +END_TEST + +START_TEST(test_duplicate_id_attribute_multiple_attlistdecl) { + /* + https://www.w3.org/TR/xml/#attdecls + + Test the following statement from the linked specification: + When more than one AttlistDecl is provided for a given element type, + the contents of all those provided are merged. + */ + const char *text = "\n" + " \n" + " \n" + "]>\n" + "\n"; + AttrInfo doc_info[] + = {{XCS("identifier"), XCS("doc_identity")}, {NULL, NULL}}; + AttrInfo tag_info[] + = {{XCS("identifier"), XCS("identifier_tag")}, {NULL, NULL}}; + ElementInfo info[] = {{XCS("doc"), 1, 0, XCS("identifier"), doc_info}, + {XCS("tag"), 0, 1, NULL, tag_info}, + {NULL, 0, 0, NULL, NULL}}; + + XML_Parser parser = XML_ParserCreate(NULL); + assert_true(parser != NULL); + + ParserAndElementInfo parserAndElementInfos = { + parser, + info, + }; + + XML_SetStartElementHandler(parser, counting_start_element_handler); + XML_SetUserData(parser, &parserAndElementInfos); + + if (_XML_Parse_SINGLE_BYTES(parser, text, (int)strlen(text), XML_TRUE) + != XML_STATUS_OK) + xml_failure(parser); + + XML_ParserFree(parser); +} +END_TEST + /* Test reset works correctly in the middle of processing an internal * entity. Exercises some obscure code in XML_ParserReset(). */ @@ -5543,8 +5814,8 @@ (long unsigned)(N_LINES - 1)); AttrInfo doc_info[] = {{XCS("name"), XCS("deepText")}, {NULL, NULL}}; - ElementInfo info[] = {{XCS("foo"), 1, NULL, NULL}, {NULL, 0, NULL, NULL}}; - info[0].attributes = doc_info; + ElementInfo info[] + = {{XCS("foo"), 1, 0, NULL, doc_info}, {NULL, 0, 0, NULL, NULL}}; XML_Parser parser = XML_ParserCreate(NULL); ParserAndElementInfo parserPlusElemenInfo = {parser, info}; @@ -6374,6 +6645,15 @@ tcase_add_test__ifdef_xml_dtd(tc_basic, test_empty_foreign_dtd); tcase_add_test(tc_basic, test_set_base); tcase_add_test(tc_basic, test_attributes); + tcase_add_test(tc_basic, test_duplicate_cdata_attribute); + tcase_add_test(tc_basic, test_duplicate_id_attribute_1); + tcase_add_test(tc_basic, test_duplicate_id_attribute_2); + tcase_add_test(tc_basic, test_duplicate_cdata_attribute_multiple_attlistdecl); + tcase_add_test(tc_basic, + test_duplicate_cdata_attribute_multiple_attlistdecl_2); + tcase_add_test(tc_basic, + test_duplicate_cdata_attribute_multiple_attlistdecl_3); + tcase_add_test(tc_basic, test_duplicate_id_attribute_multiple_attlistdecl); tcase_add_test__if_xml_ge(tc_basic, test_reset_in_entity); tcase_add_test(tc_basic, test_resume_invalid_parse); tcase_add_test(tc_basic, test_resume_resuspended); --- ./lib/xmlparse.c.orig 2026-03-17 14:51:51.000000000 -0500 +++ ./lib/xmlparse.c 2026-05-11 15:47:02.814650919 -0500 @@ -388,6 +388,7 @@ int nDefaultAtts; int allocDefaultAtts; DEFAULT_ATTRIBUTE *defaultAtts; + HASH_TABLE defaultAttsNames; } ELEMENT_TYPE; typedef struct { @@ -3853,6 +3854,8 @@ sizeof(ELEMENT_TYPE)); if (! elementType) return XML_ERROR_NO_MEMORY; + if (! elementType->defaultAttsNames.parser) + hashTableInit(&(elementType->defaultAttsNames), parser); if (parser->m_ns && ! setElementTypePrefix(parser, elementType)) return XML_ERROR_NO_MEMORY; } @@ -7186,10 +7189,10 @@ if (value || isId) { /* The handling of default attributes gets messed up if we have a default which duplicates a non-default. */ - int i; - for (i = 0; i < type->nDefaultAtts; i++) - if (attId == type->defaultAtts[i].id) - return 1; + NAMED *const nameFound + = (NAMED *)lookup(parser, &(type->defaultAttsNames), attId->name, 0); + if (nameFound) + return 1; if (isId && ! type->idAtt && ! attId->xmlns) type->idAtt = attId; } @@ -7236,6 +7239,12 @@ att->isCdata = isCdata; if (! isCdata) attId->maybeTokenized = XML_TRUE; + + NAMED *const nameAddedOrFound = (NAMED *)lookup( + parser, &(type->defaultAttsNames), attId->name, sizeof(NAMED)); + if (! nameAddedOrFound) + return 0; + type->nDefaultAtts += 1; return 1; } @@ -7561,6 +7570,7 @@ ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter); if (! e) break; + hashTableDestroy(&(e->defaultAttsNames)); if (e->allocDefaultAtts != 0) FREE(parser, e->defaultAtts); } @@ -7602,6 +7612,7 @@ ELEMENT_TYPE *e = (ELEMENT_TYPE *)hashTableIterNext(&iter); if (! e) break; + hashTableDestroy(&(e->defaultAttsNames)); if (e->allocDefaultAtts != 0) FREE(parser, e->defaultAtts); } @@ -7695,6 +7706,10 @@ sizeof(ELEMENT_TYPE)); if (! newE) return 0; + + if (! newE->defaultAttsNames.parser) + hashTableInit(&(newE->defaultAttsNames), parser); + if (oldE->nDefaultAtts) { /* Detect and prevent integer overflow. * The preprocessor guard addresses the "always false" warning @@ -7719,8 +7734,9 @@ newE->prefix = (PREFIX *)lookup(oldParser, &(newDtd->prefixes), oldE->prefix->name, 0); for (i = 0; i < newE->nDefaultAtts; i++) { + const XML_Char *const attributeName = oldE->defaultAtts[i].id->name; newE->defaultAtts[i].id = (ATTRIBUTE_ID *)lookup( - oldParser, &(newDtd->attributeIds), oldE->defaultAtts[i].id->name, 0); + oldParser, &(newDtd->attributeIds), attributeName, 0); newE->defaultAtts[i].isCdata = oldE->defaultAtts[i].isCdata; if (oldE->defaultAtts[i].value) { newE->defaultAtts[i].value @@ -7729,6 +7745,12 @@ return 0; } else newE->defaultAtts[i].value = NULL; + + NAMED *const nameAddedOrFound = (NAMED *)lookup( + parser, &(newE->defaultAttsNames), attributeName, sizeof(NAMED)); + if (! nameAddedOrFound) { + return 0; + } } } @@ -8473,6 +8495,8 @@ sizeof(ELEMENT_TYPE)); if (! ret) return NULL; + if (! ret->defaultAttsNames.parser) + hashTableInit(&(ret->defaultAttsNames), getRootParserOf(parser, NULL)); if (ret->name != name) poolDiscard(&dtd->pool); else {