From 077f52db2f8ba01d8f2e1da5a67db4b1a70d05aa Mon Sep 17 00:00:00 2001 From: Todd White Date: Wed, 22 Jul 2026 19:42:06 -0400 Subject: [PATCH] Implement CFXML entity escaping and unescaping CFXMLCreateStringByEscapingEntities and CFXMLCreateStringByUnescapingEntities returned NULL. Escaping replaces the five predefined XML entities (&, <, >, " and ') with their references and copies other characters unchanged. Unescaping resolves those five named entities, decimal and hexadecimal numeric character references, and any names present in the supplied entities dictionary. --- Source/CFXMLParser.c | 148 ++++++++++++++++++++++++++++++++++- Tests/CFXMLParser/TestInfo | 0 Tests/CFXMLParser/escaping.m | 54 +++++++++++++ 3 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 Tests/CFXMLParser/TestInfo create mode 100755 Tests/CFXMLParser/escaping.m diff --git a/Source/CFXMLParser.c b/Source/CFXMLParser.c index 2dfb525..e323b87 100644 --- a/Source/CFXMLParser.c +++ b/Source/CFXMLParser.c @@ -381,13 +381,157 @@ CFStringRef CFXMLCreateStringByEscapingEntities(CFAllocatorRef allocator, CFStringRef string, CFDictionaryRef entitiesDictionary) { - return NULL; + CFMutableStringRef result; + CFIndex length; + CFIndex idx; + + if (string == NULL) + return NULL; + + length = CFStringGetLength (string); + result = CFStringCreateMutable (allocator, 0); + for (idx = 0 ; idx < length ; ++idx) + { + UniChar c = CFStringGetCharacterAtIndex (string, idx); + switch (c) + { + case '&': + CFStringAppendCString (result, "&", kCFStringEncodingASCII); + break; + case '<': + CFStringAppendCString (result, "<", kCFStringEncodingASCII); + break; + case '>': + CFStringAppendCString (result, ">", kCFStringEncodingASCII); + break; + case '"': + CFStringAppendCString (result, """, kCFStringEncodingASCII); + break; + case '\'': + CFStringAppendCString (result, "'", kCFStringEncodingASCII); + break; + default: + CFStringAppendCharacters (result, &c, 1); + break; + } + } + + return result; } CFStringRef CFXMLCreateStringByUnescapingEntities(CFAllocatorRef allocator, CFStringRef string, CFDictionaryRef entitiesDictionary) { - return NULL; + CFMutableStringRef result; + CFIndex length; + CFIndex idx; + + if (string == NULL) + return NULL; + + length = CFStringGetLength (string); + result = CFStringCreateMutable (allocator, 0); + idx = 0; + while (idx < length) + { + UniChar c = CFStringGetCharacterAtIndex (string, idx); + Boolean handled = false; + + if (c == '&') + { + CFIndex semi = idx + 1; + + while (semi < length + && CFStringGetCharacterAtIndex (string, semi) != ';') + semi += 1; + + if (semi < length && semi > idx + 1) + { + if (CFStringGetCharacterAtIndex (string, idx + 1) == '#') + { + /* Numeric character reference, decimal or hexadecimal. */ + CFIndex pos = idx + 2; + long value = 0; + int base = 10; + + if (pos < semi + && (CFStringGetCharacterAtIndex (string, pos) == 'x' + || CFStringGetCharacterAtIndex (string, pos) == 'X')) + { + base = 16; + pos += 1; + } + for ( ; pos < semi ; ++pos) + { + UniChar d = CFStringGetCharacterAtIndex (string, pos); + int v; + + if (d >= '0' && d <= '9') + v = d - '0'; + else if (base == 16 && d >= 'a' && d <= 'f') + v = d - 'a' + 10; + else if (base == 16 && d >= 'A' && d <= 'F') + v = d - 'A' + 10; + else + { + value = -1; + break; + } + value = value * base + v; + } + if (value >= 0 && value <= 0xFFFF) + { + UniChar u = (UniChar)value; + CFStringAppendCharacters (result, &u, 1); + handled = true; + } + } + else + { + CFStringRef name; + const void *repl = NULL; + + name = CFStringCreateWithSubstring (NULL, string, + CFRangeMake (idx + 1, semi - idx - 1)); + if (CFStringCompare (name, CFSTR ("amp"), 0) + == kCFCompareEqualTo) + repl = CFSTR ("&"); + else if (CFStringCompare (name, CFSTR ("lt"), 0) + == kCFCompareEqualTo) + repl = CFSTR ("<"); + else if (CFStringCompare (name, CFSTR ("gt"), 0) + == kCFCompareEqualTo) + repl = CFSTR (">"); + else if (CFStringCompare (name, CFSTR ("quot"), 0) + == kCFCompareEqualTo) + repl = CFSTR ("\""); + else if (CFStringCompare (name, CFSTR ("apos"), 0) + == kCFCompareEqualTo) + repl = CFSTR ("'"); + else if (entitiesDictionary != NULL) + repl = CFDictionaryGetValue (entitiesDictionary, name); + + if (repl != NULL) + { + CFStringAppend (result, repl); + handled = true; + } + CFRelease (name); + } + + if (handled) + idx = semi + 1; + } + } + + if (!handled) + { + CFStringAppendCharacters (result, &c, 1); + idx += 1; + } + } + + return result; } diff --git a/Tests/CFXMLParser/TestInfo b/Tests/CFXMLParser/TestInfo new file mode 100644 index 0000000..e69de29 diff --git a/Tests/CFXMLParser/escaping.m b/Tests/CFXMLParser/escaping.m new file mode 100755 index 0000000..f064cc7 --- /dev/null +++ b/Tests/CFXMLParser/escaping.m @@ -0,0 +1,54 @@ +#include "CoreFoundation/CFXMLParser.h" +#include "CoreFoundation/CFString.h" + +#include "../CFTesting.h" + +int +main (void) +{ + CFStringRef esc; + CFStringRef un; + + /* Each of the five predefined entities is escaped, other text is left + * alone. + */ + esc = CFXMLCreateStringByEscapingEntities (NULL, CFSTR ("ac&d\"e'f"), + NULL); + PASS_CFEQ(esc, CFSTR ("a<b>c&d"e'f"), + "The five predefined entities are escaped."); + + /* Unescaping is the inverse. */ + un = CFXMLCreateStringByUnescapingEntities (NULL, esc, NULL); + PASS_CFEQ(un, CFSTR ("ac&d\"e'f"), + "Unescaping the escaped string round-trips."); + CFRelease (un); + CFRelease (esc); + + /* A string with no markup characters is unchanged. */ + esc = CFXMLCreateStringByEscapingEntities (NULL, CFSTR ("plain text 123"), + NULL); + PASS_CFEQ(esc, CFSTR ("plain text 123"), + "Text without markup characters is copied verbatim."); + CFRelease (esc); + + /* The empty string escapes to the empty string. */ + esc = CFXMLCreateStringByEscapingEntities (NULL, CFSTR (""), NULL); + PASS_CFEQ(esc, CFSTR (""), "The empty string escapes to itself."); + CFRelease (esc); + + /* Unescaping resolves named entities and numeric character references. */ + un = CFXMLCreateStringByUnescapingEntities (NULL, + CFSTR ("x & y <z> A "q" '"), NULL); + PASS_CFEQ(un, CFSTR ("x & y A \"q\" '"), + "Named entities and a decimal character reference are resolved."); + CFRelease (un); + + /* A hexadecimal character reference resolves too. */ + un = CFXMLCreateStringByUnescapingEntities (NULL, CFSTR ("AB"), + NULL); + PASS_CFEQ(un, CFSTR ("AB"), + "A hexadecimal character reference is resolved."); + CFRelease (un); + + return 0; +}