diff --git a/src/core/file_format.rs b/src/core/file_format.rs index dc1915a43..35d407555 100644 --- a/src/core/file_format.rs +++ b/src/core/file_format.rs @@ -249,6 +249,9 @@ pub enum FileFormat { /// Plain text format (.txt) TXT, + /// HTML / XHTML document (.htm, .html, .xhtml) + HTML, + /// Windows shortcut (.lnk) LNK, @@ -378,6 +381,7 @@ impl FileFormat { FileFormat::ICS => "iCalendar", FileFormat::EML => "EML", FileFormat::TXT => "TXT", + FileFormat::HTML => "HTML", FileFormat::LNK => "Windows Shortcut", FileFormat::SQLite => "SQLite", FileFormat::Prefetch => "Windows Prefetch", @@ -478,6 +482,7 @@ impl FileFormat { FileFormat::ICS => &["ics", "ical"], FileFormat::EML => &["eml", "email"], FileFormat::TXT => &["txt", "text"], + FileFormat::HTML => &["htm", "html", "xhtml"], FileFormat::LNK => &["lnk"], FileFormat::SQLite => &["db", "sqlite", "sqlite3"], FileFormat::Prefetch => &["pf"], diff --git a/src/core/format_dispatch.rs b/src/core/format_dispatch.rs index 0805400f2..b677b7d89 100644 --- a/src/core/format_dispatch.rs +++ b/src/core/format_dispatch.rs @@ -68,6 +68,7 @@ use crate::parsers::specialized::sqlite::parse_sqlite_metadata; use crate::parsers::specialized::stl::parse_stl_metadata; use crate::parsers::specialized::x509::parse_x509_metadata; use crate::parsers::text::eps::parse_eps_metadata; +use crate::parsers::text::html::parse_html_metadata; use crate::parsers::text::txt::parse_txt_metadata; use crate::parsers::text::vcf::parse_vcf_metadata; use crate::parsers::video::asf::parse_asf_metadata; @@ -170,6 +171,7 @@ pub fn dispatch_format_parser(reader: &dyn FileReader, format: FileFormat) -> Re FileFormat::HDF5 => convert_string_error(parse_hdf5_metadata(reader), "HDF5"), FileFormat::VCF => convert_string_error(parse_vcf_metadata(reader), "VCF"), FileFormat::TXT => convert_string_error(parse_txt_metadata(reader), "TXT"), + FileFormat::HTML => convert_string_error(parse_html_metadata(reader), "HTML"), FileFormat::LNK => convert_string_error(parse_lnk_metadata(reader), "LNK"), FileFormat::SQLite => convert_string_error(parse_sqlite_metadata(reader), "SQLite"), FileFormat::ICS => convert_string_error(parse_ics_metadata(reader), "ICS"), diff --git a/src/parsers/detection/mod.rs b/src/parsers/detection/mod.rs index b5946145e..e2d31902f 100644 --- a/src/parsers/detection/mod.rs +++ b/src/parsers/detection/mod.rs @@ -266,6 +266,18 @@ pub fn detect_format(reader: &dyn FileReader) -> io::Result { return Ok(FileFormat::Plist); } + // HTML and XHTML, using ExifTool's own gate from `HTML.pm`'s ProcessHTML. + // It runs after the SVG and plist roots because those three share the + // ` String { + pub(crate) fn decode_mac_roman(data: &[u8]) -> String { const MAC_ROMAN_HIGH: [char; 128] = [ 'Ä', 'Å', 'Ç', 'É', 'Ñ', 'Ö', 'Ü', 'á', 'à', 'â', 'ä', 'ã', 'å', 'ç', 'é', 'è', 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ñ', 'ó', 'ò', 'ô', 'ö', 'õ', 'ú', 'ù', 'û', 'ü', '†', '°', diff --git a/src/parsers/text/html.rs b/src/parsers/text/html.rs new file mode 100644 index 000000000..eae9e084d --- /dev/null +++ b/src/parsers/text/html.rs @@ -0,0 +1,1610 @@ +//! HTML / XHTML `` meta information parser. +//! +//! A transcription of ExifTool's `HTML.pm` (`ProcessHTML`, ExifTool 13.55). +//! Everything ExifTool reports for an HTML file comes out of the document +//! head: `META` elements, the `TITLE` element, and the MS-Office `XML` island +//! that Word and Excel write inside a `\n\ + \n", + ); + + let metadata = HTMLParser::parse_bytes(&doc); + + // One file, five family-1 groups. + assert_eq!( + value(&metadata, "HTML:Title").as_deref(), + Some("ExifTool HTML Test") + ); + assert_eq!( + value(&metadata, "HTTP-equiv:ContentType").as_deref(), + Some("text/html; charset=\"iso-8859-1\"") + ); + assert_eq!( + value(&metadata, "HTML-ncc:Duration").as_deref(), + Some("91:27:21") + ); + assert_eq!( + value(&metadata, "HTML-prod:RecEngineer").as_deref(), + Some("P Harvey") + ); + + // dc:creator is a Seq, so two META elements collapse into one list. + assert_eq!( + metadata.get("HTML-dc:Creator"), + Some(&TagValue::Array(vec![ + TagValue::String("Phil Harvey".into()), + TagValue::String("Another Creator".into()), + ])) + ); + // dc:date carries only ConvertDateTime, which is the identity here -- + // the malformed date is reported exactly as written. + assert_eq!( + value(&metadata, "HTML-dc:Date").as_deref(), + Some("2007-30-01") + ); + assert_eq!( + value(&metadata, "HTML-dc:Subject").as_deref(), + Some("Greek: α β γ") + ); + + // The Office island: charset-decoded, XML-unescaped, converted. + assert_eq!( + value(&metadata, "HTML-office:Category").as_deref(), + Some("a catégory") + ); + assert_eq!( + value(&metadata, "HTML-office:Description").as_deref(), + Some("a comments\ra new line") + ); + assert_eq!( + value(&metadata, "HTML-office:TotalEditTime").as_deref(), + Some("1 minute") + ); + assert_eq!( + value(&metadata, "HTML-office:CreateDate").as_deref(), + Some("2010:06:28 23:52:00Z") + ); + // o:Revision and o:Version are both RevisionNumber and neither is a + // list, so the later element wins. + assert_eq!( + value(&metadata, "HTML-office:RevisionNumber").as_deref(), + Some("12.0") + ); + // A custom property not in the table gets its name derived. + assert_eq!( + value(&metadata, "HTML-office:CheckedBy").as_deref(), + Some("Phil") + ); + } + + #[test] + fn a_document_without_a_head_yields_nothing() { + let metadata = HTMLParser::parse_bytes(b"\nno head here\n\n"); + assert_eq!(metadata.len(), 0); + } +} diff --git a/src/parsers/text/mod.rs b/src/parsers/text/mod.rs index 2c1811330..555e83d96 100644 --- a/src/parsers/text/mod.rs +++ b/src/parsers/text/mod.rs @@ -3,9 +3,11 @@ #![allow(dead_code)] pub mod eps; +pub mod html; pub mod txt; pub mod vcf; pub use eps::{EPSParser, parse_eps_metadata}; +pub use html::{HTMLParser, parse_html_metadata}; pub use txt::{TXTParser, parse_txt_metadata}; pub use vcf::VCFParser;