Its a crux!

chore: add hooks for data loading support in plist-parser

Signed-off-by: Jonathan Basniak <740416+gm112@users.noreply.github.com>

+112 -28
+71
libraries/plist-parser/src/plist-parser.test.ts
··· 93 93 { CFBundleName: function () {} }, 94 94 { CFBundleName: undefined }, 95 95 { Items: ['ok', null] }, 96 + { CFBundleName: new Uint8Array([1, 2, 3]) }, 96 97 123, 97 98 ].map((value, index) => it(`throws_on_unsupported_value_type_${index + 1}`, () => { 98 99 expect(() => ··· 184 185 </array> 185 186 </plist> 186 187 `.trim(), 188 + `<?xml version="1.0" encoding="UTF-8"?> 189 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 190 + <plist version="1.0"> 191 + <array> 192 + <data>123</data> 193 + </array> 194 + </plist> 195 + `.trim(), 187 196 ].map((xml, index) => it(`throws on malformed array content_${index + 1}`, () => 188 197 expect(() => deserialize_plist_xml_to_plist_object(xml)).toThrowError(/unsupported_tag|invalid_xml/), 189 198 )) ··· 229 238 ].map((xml, index) => it(`throws on malformed dict content_${index + 1}`, () => 230 239 expect(() => deserialize_plist_xml_to_plist_object(xml)).toThrowError(/invalid_xml/), 231 240 )) 241 + 242 + const _malformed_dates_xml = [ 243 + ` 244 + <?xml version="1.0" encoding="UTF-8"?> 245 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 246 + <plist version="1.0"> 247 + <dict> 248 + <key>CFBundleName</key> 249 + <date>2023-09-25T14:60:00Z</date> 250 + </dict> 251 + </plist> 252 + `.trim(), 253 + ].map((xml, index) => it(`throws on malformed date content_${index + 1}`, () => 254 + expect(() => deserialize_plist_xml_to_plist_object(xml)).toThrowError(/invalid_xml/), 255 + )) 256 + 257 + const _malformed_reals_xml = [ 258 + ` 259 + <?xml version="1.0" encoding="UTF-8"?> 260 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 261 + <plist version="1.0"> 262 + <dict> 263 + <key>CFBundleName</key> 264 + <real>123.pizza</real> 265 + </dict> 266 + </plist> 267 + `.trim(), 268 + ].map((xml, index) => it(`throws on malformed real content_${index + 1}`, () => 269 + expect(() => deserialize_plist_xml_to_plist_object(xml)).toThrowError(/invalid_xml/), 270 + )) 271 + 272 + const _malformed_numbers_xml = [ 273 + ` 274 + <?xml version="1.0" encoding="UTF-8"?> 275 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 276 + <plist version="1.0"> 277 + <dict> 278 + <key>CFBundleName</key> 279 + <integer>123.pizza</integer> 280 + </dict> 281 + </plist> 282 + `.trim(), 283 + ].map((xml, index) => it(`throws on malformed number content_${index + 1}`, () => 284 + expect(() => deserialize_plist_xml_to_plist_object(xml)).toThrowError(/invalid_xml/), 285 + )) 286 + 287 + const _malformed_booleans_xml = [ 288 + ` 289 + <?xml version="1.0" encoding="UTF-8"?> 290 + <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 291 + <plist version="1.0"> 292 + <dict> 293 + <key>CFBundleName</key> 294 + <true>123.pizza</true> 295 + </dict> 296 + </plist> 297 + `.trim(), 298 + ].map((xml, index) => it(`throws on malformed boolean content_${index + 1}`, () => 299 + expect(() => deserialize_plist_xml_to_plist_object(xml)).toThrowError(/invalid_xml/), 300 + )) 301 + 302 + // 232 303 }) 233 304 }) 234 305 })
+41 -28
libraries/plist-parser/src/plist-parser.ts
··· 5 5 * @see {serialize_xml_to_plist_object} 6 6 * @see {deserialize_plist_xml_to_plist_object} 7 7 * @link https://code.google.com/archive/p/networkpx/wikis/PlistSpec.wiki 8 + * @link https://www.apple.com/DTDs/PropertyList-1.0.dtd 8 9 * @description A single-file zero dependency parser for serializing and deserializing Apple Info.plist files. Supprts only XML formatted plists. 9 10 * Only supports plists with a single root element, that contain only string, number, boolean, reals, dates, array(also nested), and dictionary(also nested) values. 10 - * Attempts to conform to Apple's formatting and specifications for plists in the parts of the spec that are supported. 11 + * Attempts to conform to Apple's formatting and specifications for plists in the parts of the spec that are supported. Comments are ignored and will no-op/be removed when deserializing. 11 12 * 12 13 * Notes for things that are not supported: 13 14 * - Does not care about UTF-16 encoding support. Untested, but might work fine. 14 15 * - Large files probably won't work due to the use of using regex to extract the <plist> data. Changing deserialization to use a streaming parser would address this if it is an issue. 15 - * - Comments are ignored and will no-op/be removed when deserializing. 16 16 * - Binary data is ignored, and is untested. 17 - * - All unknown key dictionaries are not supported, and may be treated as a regular JSON object, which may mess up the structure of the plist. 18 17 */ 19 18 20 19 /** 21 20 * Error types that plist_parser can throw. 22 21 */ 23 22 export type plist_parser_error_types = 'unsupported_value_type' | 'unsupported_tag' | 'invalid_xml' | 'info_plist_not_found' | 'infinite_loop' 24 - export type plist_value = string | Date | number | boolean | plist_value[] | { [key: string]: plist_value } 23 + /** 24 + * The plist value type. 25 + * @see https://www.apple.com/DTDs/PropertyList-1.0.dtd 26 + * @todo Add support for binary data. 27 + */ 28 + export type plist_value = string | Uint8Array | Date | number | boolean | plist_value[] | { [key: string]: plist_value } 25 29 26 30 /** 27 31 * Serialize JS object to plist XML ··· 82 86 else if (value !== null && value !== undefined) 83 87 if (value instanceof Date) 84 88 return serialize_date(value as Date, indent) 89 + else if (value instanceof Uint8Array) 90 + return serialize_data(value as Uint8Array, indent) 85 91 else 86 92 return serialize_object(value as Record<string, plist_value>, indent, depth) 87 93 } ··· 110 116 111 117 function serialize_boolean(value: boolean, indent: string) { 112 118 return `${indent}${value ? '<true/>' : '<false/>'}` 119 + } 120 + 121 + function serialize_data(value: Uint8Array, _indent: string): string { 122 + throw new Error('unsupported_value_type' as plist_parser_error_types, { cause: { value, type: typeof value } }) 113 123 } 114 124 115 125 function serialize_array(value: plist_value[], indent: string, depth: number) { ··· 153 163 return open !== -1 && xml_fragment[open + 1] === '/' && xml_fragment[xml_fragment.length - 1] === '>' 154 164 } 155 165 156 - const plist_date_iso8601_regex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/ 157 166 function deserialize_plist_date_to_date_object(xml_fragment: string) { 158 - const match = xml_fragment.match(plist_date_iso8601_regex) 159 - if (!match) throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: xml_fragment } }) 167 + const timestampe_date = new Date(xml_fragment) 168 + if (timestampe_date.toString() === 'Invalid Date') throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: xml_fragment } }) 169 + 170 + return timestampe_date 171 + } 172 + 173 + function deserialize_plist_integer_to_number_object(content: string) { 174 + const number_value = Number(content) 175 + if (Number.isSafeInteger(number_value)) 176 + return parseInt(content!) 177 + 178 + throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: content } }) 179 + } 180 + 181 + function deserialize_plist_real_to_number_object(content: string) { 182 + const number_value = Number(content) 183 + if (Number.isFinite(number_value)) 184 + return number_value 160 185 161 - const [, year, month, day, hour, minute, second] = match 162 - return new Date(Date.UTC( 163 - Number(year), 164 - Number(month) - 1, 165 - Number(day), 166 - Number(hour), 167 - Number(minute), 168 - Number(second), 169 - )) 186 + throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: content } }) 187 + } 188 + 189 + function deserialize_plist_data_to_data_object(content: string): plist_value { 190 + throw new Error('unsupported_tag' as plist_parser_error_types, { cause: { xml: content } }) 170 191 } 171 192 172 193 function deserialize_xml_fragment_to_plist_value_object(xml_fragment: string): plist_value { 173 194 if (xml_fragment === '<true/>' || xml_fragment === '<false/>') return xml_fragment === '<true/>' 174 195 else if (xml_fragment === '<array/>') return [] 175 196 else if (xml_fragment === '<dict/>') return {} 176 - else if (xml_fragment === '<date/>') return new Date(0) 177 197 else if (!naive_ends_with_closing_tag(xml_fragment, '')) throw new Error('invalid_xml' as plist_parser_error_types, { cause: { xml: xml_fragment } }) 178 198 179 199 const match = xml_fragment.match(plist_parser_regex) ··· 188 208 else if (tag === 'array') return deserialize_plist_array_to_object(content!) 189 209 else if (tag === 'date') return deserialize_plist_date_to_date_object(content!) 190 210 else if (tag === 'true' || tag === 'false') return tag === 'true' 191 - else if (tag === 'integer') { 192 - const number_value = Number(content!) 193 - if (Number.isSafeInteger(number_value)) 194 - return parseInt(content!) 195 - } 196 - else if (tag === 'real') { 197 - const number_value = Number(content!) 198 - if (Number.isFinite(number_value)) 199 - return number_value 200 - } 211 + else if (tag === 'integer') return deserialize_plist_integer_to_number_object(content!) 212 + else if (tag === 'real') return deserialize_plist_real_to_number_object(content!) 213 + else if (tag === 'data') return deserialize_plist_data_to_data_object(content!) 201 214 202 215 throw new Error('invalid_xml' as plist_parser_error_types, { 203 - cause: { xml: xml_fragment }, 216 + cause: { xml: xml_fragment, tag }, 204 217 }) 205 218 } 206 219