@@ -549,17 +549,251 @@ describe("xml", function()
549549 assert .same (" "'<>&" , esc )
550550 end )
551551
552+
553+ it (" escapes non-printable characters as \\ xHH" , function ()
554+ -- Test null byte
555+ local esc = xml .xml_escape (" hello\000 world" )
556+ assert .same (" hello\\ x00world" , esc )
557+
558+ -- Test control characters
559+ local esc2 = xml .xml_escape (" \001\002\003 " )
560+ assert .same (" \\ x01\\ x02\\ x03" , esc2 )
561+
562+ -- Test DEL character
563+ local esc3 = xml .xml_escape (" test\127 end" )
564+ assert .same (" test\\ x7Fend" , esc3 )
565+ end )
566+
567+
568+ it (" preserves tab, newline, carriage return" , function ()
569+ local esc = xml .xml_escape (" hello\t world\n " )
570+ assert .same (" hello\t world\n " , esc )
571+
572+ local esc2 = xml .xml_escape (" line1\r\n line2" )
573+ assert .same (" line1\r\n line2" , esc2 )
574+ end )
575+
576+
577+ it (" escapes high ASCII characters (127-255)" , function ()
578+ -- Only DEL (127) should be escaped, high bytes (128-255) are preserved for UTF-8
579+ -- Note: Using decimal escape sequences (\ddd) for Lua 5.1 compatibility
580+ -- Lua 5.1 doesn't support \xHH hex escapes
581+ local esc = xml .xml_escape (" test\127 " )
582+ assert .same (" test\\ x7F" , esc )
583+
584+ -- High bytes preserved
585+ local esc2 = xml .xml_escape (" test\128\255 " )
586+ assert .same (" test\128\255 " , esc2 )
587+ end )
588+
589+
590+ it (" handles mixed content with both special and non-printable chars" , function ()
591+ -- \0 = null byte, \1 = SOH (using decimal escapes for Lua 5.1)
592+ local esc = xml .xml_escape (" hello\0 <tag>&\1 world" )
593+ assert .same (" hello\\ x00<tag>&\\ x01world" , esc )
594+ end )
595+
596+
597+ it (" handles UTF-8 text correctly" , function ()
598+ -- UTF-8 multi-byte characters should be preserved (not escaped)
599+ local esc = xml .xml_escape (" 你好世界" )
600+ assert .same (" 你好世界" , esc )
601+
602+ local esc2 = xml .xml_escape (" hello 世界 <tag>" )
603+ assert .same (" hello 世界 <tag>" , esc2 )
604+ end )
605+
606+
607+ it (" handles empty string" , function ()
608+ local esc = xml .xml_escape (" " )
609+ assert .same (" " , esc )
610+ end )
611+
612+
613+ it (" handles string with only printable characters" , function ()
614+ local esc = xml .xml_escape (" Hello World 123!" )
615+ assert .same (" Hello World 123!" , esc )
616+ end )
617+
618+
619+ it (" escapes binary data in text nodes" , function ()
620+ local doc = xml .new (" data" )
621+ -- \0=NUL, \1=SOH, \2=STX, \127=DEL (decimal escapes for Lua 5.1)
622+ doc :text (" \0\1\2\127 " )
623+ assert .same (" <data>\\ x00\\ x01\\ x02\\ x7F</data>" , doc :tostring ())
624+ end )
625+
626+
627+ it (" escapes binary data in attributes" , function ()
628+ -- \0 = null byte (decimal escape for Lua 5.1)
629+ local doc = xml .new (" data" , { content = " hello\0 world" })
630+ assert .same (" <data content='hello\\ x00world'/>" , doc :tostring ())
631+ end )
632+
633+
634+ it (" handles real binary data: all control characters" , function ()
635+ -- Generate a string with all control characters (0-31, excluding 9, 10, 13)
636+ local control_chars = {}
637+ for i = 0 , 31 do
638+ if i ~= 9 and i ~= 10 and i ~= 13 then -- exclude tab, LF, CR
639+ table.insert (control_chars , string.char (i ))
640+ end
641+ end
642+ table.insert (control_chars , string.char (127 )) -- DEL
643+ local binary_data = table.concat (control_chars )
644+
645+ local escaped = xml .xml_escape (binary_data )
646+ -- Verify all control chars are escaped
647+ assert .is_true (escaped :match (" \\ x00" ) ~= nil )
648+ assert .is_true (escaped :match (" \\ x7F" ) ~= nil )
649+ -- Should not contain raw control chars
650+ -- Use string.find with plain text search instead of pattern match (Lua 5.1 compatible)
651+ assert .is_false (string.find (escaped , string.char (0 ), 1 , true ) ~= nil )
652+ end )
653+
654+
655+ it (" handles real binary data: simulated file header" , function ()
656+ -- Simulate a PNG file header: \x89PNG\r\n\x1A\n
657+ local png_header = string.char (0x89 ) .. " PNG" .. string.char (0x0D , 0x0A , 0x1A , 0x0A )
658+ local doc = xml .new (" file" , { format = " png" })
659+ doc :text (png_header )
660+
661+ local result = doc :tostring ()
662+ -- \137 is high byte (0x89), preserved for UTF-8, won't be escaped
663+ -- Using decimal escapes: \137=0x89, \13=CR, \10=LF (Lua 5.1 compatible)
664+ assert .is_true (result :match (" \137 " ) ~= nil )
665+ assert .is_true (result :match (" PNG" ) ~= nil )
666+ assert .is_true (result :match (" \13\10 " ) ~= nil ) -- CRLF preserved
667+ assert .is_true (result :match (" \\ x1A" ) ~= nil ) -- SUB (0x1A) escaped
668+ end )
669+
670+
671+ it (" handles real binary data: mixed binary and text" , function ()
672+ -- Simulate binary data with embedded text (like in some protocols)
673+ local data = " START" .. string.char (0x00 , 0x01 , 0x02 ) .. " MIDDLE" .. string.char (0x03 , 0x04 ) .. " END"
674+ local escaped = xml .xml_escape (data )
675+
676+ assert .same (" START\\ x00\\ x01\\ x02MIDDLE\\ x03\\ x04END" , escaped )
677+ end )
678+
679+
680+ it (" handles real binary data: random binary sequence" , function ()
681+ -- Generate random-like binary data
682+ local binary = {}
683+ local test_bytes = {0x00 , 0x01 , 0x05 , 0x0E , 0x1F , 0x7F , 0xFF , 0xFE , 0x80 }
684+ for _ , b in ipairs (test_bytes ) do
685+ table.insert (binary , string.char (b ))
686+ end
687+ local data = table.concat (binary )
688+
689+ local escaped = xml .xml_escape (data )
690+ -- Control chars should be escaped
691+ assert .is_true (escaped :match (" \\ x00" ) ~= nil )
692+ assert .is_true (escaped :match (" \\ x7F" ) ~= nil )
693+ -- High bytes (128-255) should be preserved for UTF-8
694+ assert .is_true (escaped :match (string.char (0xFF )) ~= nil )
695+ assert .is_true (escaped :match (string.char (0x80 )) ~= nil )
696+ end )
697+
698+
699+ it (" handles real binary data: protocol packet" , function ()
700+ -- Simulate a simple binary protocol packet
701+ -- Format: [STX(0x02)] [LENGTH] [DATA] [ETX(0x03)] [CHECKSUM]
702+ local STX = string.char (0x02 )
703+ local ETX = string.char (0x03 )
704+ local data = " Hello"
705+ local length = string.char (# data )
706+ local checksum = string.char (0xFF )
707+ local packet = STX .. length .. data .. ETX .. checksum
708+
709+ local escaped = xml .xml_escape (packet )
710+ assert .same (" \\ x02\\ x05Hello\\ x03" .. string.char (0xFF ), escaped )
711+ end )
712+
552713 end )
553714
554715
555716
556717 describe (" xml_unescape()" , function ()
557718
558- it (" escapes reserved characters" , function ()
719+ it (" unescapes reserved characters" , function ()
559720 local unesc = xml .xml_unescape (" "'<>&" )
560721 assert .same ([[ "'<>&]] , unesc )
561722 end )
562723
724+
725+ it (" unescapes \\ xHH control character sequences" , function ()
726+ -- Using decimal escapes in expected values for Lua 5.1: \0=NUL, \1=SOH, etc.
727+ local unesc = xml .xml_unescape (" hello\\ x00world" )
728+ assert .same (" hello\0 world" , unesc )
729+
730+ local unesc2 = xml .xml_unescape (" \\ x01\\ x02\\ x03" )
731+ assert .same (" \1\2\3 " , unesc2 )
732+ end )
733+
734+
735+ it (" unescapes mixed XML entities and \\ xHH sequences" , function ()
736+ -- Expected string uses decimal escapes: \0=NUL, \1=SOH (Lua 5.1 compatible)
737+ local unesc = xml .xml_unescape (" hello\\ x00<tag>&\\ x01world" )
738+ assert .same (" hello\0 <tag>&\1 world" , unesc )
739+ end )
740+
741+ end )
742+
743+
744+
745+ describe (" xml escape/unescape roundtrip" , function ()
746+
747+ it (" roundtrips mixed content" , function ()
748+ -- Original string uses decimal escapes: \0=NUL, \1=SOH (Lua 5.1 compatible)
749+ local original = " hello\0 <tag>&\1 world"
750+ local escaped = xml .xml_escape (original )
751+ assert .same (" hello\\ x00<tag>&\\ x01world" , escaped )
752+ local unescaped = xml .xml_unescape (escaped )
753+ assert .same (original , unescaped )
754+ end )
755+
756+
757+ it (" roundtrips binary protocol packet" , function ()
758+ local STX = string.char (0x02 )
759+ local ETX = string.char (0x03 )
760+ local original = STX .. string.char (0x05 ) .. " Hello" .. ETX .. string.char (0xFF )
761+
762+ local escaped = xml .xml_escape (original )
763+ local unescaped = xml .xml_unescape (escaped )
764+ assert .same (original , unescaped )
765+ end )
766+
767+
768+ it (" roundtrips all control characters" , function ()
769+ -- Generate all control characters (excluding tab, LF, CR)
770+ local control_chars = {}
771+ for i = 0 , 31 do
772+ if i ~= 9 and i ~= 10 and i ~= 13 then
773+ table.insert (control_chars , string.char (i ))
774+ end
775+ end
776+ table.insert (control_chars , string.char (127 )) -- DEL
777+ local original = table.concat (control_chars )
778+
779+ local escaped = xml .xml_escape (original )
780+ local unescaped = xml .xml_unescape (escaped )
781+ assert .same (original , unescaped )
782+ end )
783+
784+
785+ it (" roundtrips in XML document context" , function ()
786+ -- escape -> serialize -> parse -> unescape
787+ local original_text = " data\x00 with\x01 binary<>&"
788+ local doc = xml .new (" test" )
789+ doc :text (original_text )
790+
791+ local xml_string = doc :tostring ()
792+ local escaped_text = xml_string :match (" <test>(.-)</test>" )
793+ local recovered_text = xml .xml_unescape (escaped_text )
794+ assert .same (original_text , recovered_text )
795+ end )
796+
563797 end )
564798
565799
0 commit comments