libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

commit fe6d657e9a401ed1a33ae28d33b8da38dfd1f530
parent 3fc554ef0d3fdf19da5a4eccaa677cf309badcf3
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed, 29 Jul 2026 00:54:00 +0200

add RTF plugin

Diffstat:
M.gitignore | 1+
Mdoc/libextractor.texi | 3+++
Mpo/POTFILES.in | 2++
Msrc/plugins/Makefile.am | 22++++++++++++++++++++++
Asrc/plugins/rtf_extractor.c | 2083+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/test_rtf.c | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/plugins/testdata/README | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/testdata/rtf_doe_mac.rtf | 2++
Asrc/plugins/testdata/rtf_fda.rtf | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/testdata/rtf_fta.rtf | 46++++++++++++++++++++++++++++++++++++++++++++++
Asrc/plugins/testdata/rtf_nasa.rtf | 29+++++++++++++++++++++++++++++
Asrc/plugins/testdata/rtf_nih.rtf | 39+++++++++++++++++++++++++++++++++++++++
Asrc/plugins/testdata/rtf_review.rtf | 35+++++++++++++++++++++++++++++++++++
13 files changed, 2676 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -118,6 +118,7 @@ src/plugins/test_qt src/plugins/test_real src/plugins/test_riff src/plugins/test_rpm +src/plugins/test_rtf src/plugins/test_s3m src/plugins/test_sid src/plugins/test_thumbnailgtk diff --git a/doc/libextractor.texi b/doc/libextractor.texi @@ -831,6 +831,9 @@ RIFF @item RPM (using librpm) @item +RTF (@file{.rtf}, including the revision table, comment authors and +custom document properties) +@item S3M @item SID diff --git a/po/POTFILES.in b/po/POTFILES.in @@ -28,6 +28,7 @@ src/plugins/jpeg_extractor.c src/plugins/man_extractor.c src/plugins/midi_extractor.c src/plugins/mime_extractor.c +src/plugins/msoffice_extractor.c src/plugins/qt_extractor.c src/plugins/mpeg_extractor.c src/plugins/nsfe_extractor.c @@ -39,6 +40,7 @@ src/plugins/png_extractor.c src/plugins/ps_extractor.c src/plugins/riff_extractor.c src/plugins/rpm_extractor.c +src/plugins/rtf_extractor.c src/plugins/s3m_extractor.c src/plugins/sid_extractor.c src/plugins/thumbnailgtk_extractor.c diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am @@ -65,6 +65,12 @@ EXTRA_DIST = \ testdata/ra3.ra \ testdata/riff_flame.avi \ testdata/rpm_test.rpm \ + testdata/rtf_doe_mac.rtf \ + testdata/rtf_fda.rtf \ + testdata/rtf_fta.rtf \ + testdata/rtf_nasa.rtf \ + testdata/rtf_nih.rtf \ + testdata/rtf_review.rtf \ testdata/s3m_2nd_pm.s3m \ testdata/applefile_test.applesingle \ testdata/sid_wizball.sid \ @@ -189,6 +195,7 @@ plugin_LTLIBRARIES = \ libextractor_ps.la \ libextractor_real.la \ libextractor_riff.la \ + libextractor_rtf.la \ libextractor_s3m.la \ libextractor_sid.la \ libextractor_wav.la \ @@ -229,6 +236,7 @@ check_PROGRAMS = \ test_png \ test_real \ test_riff \ + test_rtf \ test_s3m \ test_sid \ test_wav \ @@ -648,6 +656,20 @@ test_riff_LDADD = \ $(top_builddir)/src/plugins/libtest.la +libextractor_rtf_la_SOURCES = \ + rtf_extractor.c +libextractor_rtf_la_LDFLAGS = \ + $(PLUGINFLAGS) +libextractor_rtf_la_LIBADD = \ + $(top_builddir)/src/common/libextractor_common.la $(XLIB) $(LE_LIBINTL) + + +test_rtf_SOURCES = \ + test_rtf.c +test_rtf_LDADD = \ + $(top_builddir)/src/plugins/libtest.la + + libextractor_rpm_la_SOURCES = \ rpm_extractor.c libextractor_rpm_la_LDFLAGS = \ diff --git a/src/plugins/rtf_extractor.c b/src/plugins/rtf_extractor.c @@ -0,0 +1,2083 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/rtf_extractor.c + * @brief plugin to support Rich Text Format documents + * @author Christian Grothoff + * + * RTF is the interchange format Microsoft Word has written since 1987 + * and, unlike the binary and OOXML formats, it is plain text -- which + * is precisely why so much of it is still around, and why the meta + * data it carries is so easy to overlook. + * + * Besides the document summary in the `\info' destination, an RTF file + * routinely records: + * + * - `\author' and `\operator', that is who created the document and + * who last saved it. These are frequently *different* people. + * - `\*\company', the organisation the authoring copy of Word was + * licensed to. + * - `\creatim', `\revtim' and `\printim', in *local* time and without + * a time zone, which leaks the author's time zone when compared to + * any absolute time stamp in the same document. + * - `\*\revtbl', a table naming everybody who ever made a tracked + * change, plus `\revauth' / `\revdttm' (and the `\crauth', `\trauth' + * and `\pnrauth' variants) which index into it and carry the date + * and time of the individual edit. + * - `\*\atnauthor' and `\*\atnid', the name and initials behind each + * comment. + * - `\*\userprops', where Outlook leaves `_AuthorEmail' and + * `_AuthorEmailDisplayName' behind when a document is circulated for + * review. + * - `\*\generator' and `\*\template', naming the exact application + * build and, not rarely, a local file system path. + * + * In a sample of the 1125 RTF files of the govdocs1 corpus, 60% carry + * an `\author', 64% an `\operator', 53% a `\*\company' and 11% a + * revision table. See https://bugs.gnunet.org/view.php?id=2096. + * + * The parser is a streaming RTF tokenizer: it never holds more than a + * fixed-size window of the file, skips the destinations that carry no + * meta data (font tables, pictures, embedded objects and every unknown + * ignorable destination), and bounds the number of groups, revision + * table entries and reported values it is willing to track. This + * keeps it safe on hostile input, which matters because it runs on + * whatever a privacy tool is asked to inspect. + */ +#include "platform.h" +#include <ctype.h> +#include "extractor.h" +#include "convert.h" + + +/** + * Name this plugin reports itself as. + */ +#define PLUGIN_NAME "rtf" + +/** + * Longest meta data value we report. Also bounds the buffer we + * accumulate destination text in. + */ +#define MAX_VALUE 4096 + +/** + * Deepest group nesting we track individually. Anything below is + * still counted, so that the matching closing braces are recognised, + * but inherits the state of the deepest tracked group. + */ +#define MAX_DEPTH 128 + +/** + * Longest control word we accept. The specification limits control + * words to 32 letters. + */ +#define MAX_CW 32 + +/** + * Number of distinct strings we remember in order to suppress + * duplicates. Documents with tracked changes repeat the same handful + * of names thousands of times. + */ +#define MAX_SEEN 256 + +/** + * Number of entries we keep from the revision table. + */ +#define MAX_REVAUTHORS 64 + +/** + * Number of distinct (author, date) pairs we keep from the revision + * marks in the body. They are resolved against the revision table + * once the whole file has been read, as the table is allowed to appear + * after the marks that use it. + */ +#define MAX_REVISIONS 64 + +/** + * Maximum number of bytes we are willing to read from one file. + */ +#define MAX_SCAN_BYTES (64 * 1024 * 1024) + +/** + * Number of bytes we request from the datasource at a time. + */ +#define READ_CHUNK (64 * 1024) + + +/** + * Destinations we distinguish. Everything not listed here is either + * transparent (text in it belongs to the enclosing destination) or + * skipped wholesale. + */ +enum Destination +{ + /** + * Nothing in particular; text is discarded. + */ + DEST_NONE = 0, + + /** + * Content of this group is of no interest at all. + */ + DEST_SKIP, + + /** + * The `\info' group; holds the document summary. + */ + DEST_INFO, + + /** + * The `\*\revtbl' group; each of its subgroups names one author. + */ + DEST_REVTBL, + + /** + * The `\*\userprops' group; holds custom document properties. + */ + DEST_USERPROPS, + + /** + * The `\*\annotation' group. We have no use for the text of a + * comment, but we do want to see the `\*\atndate' inside it, so it + * must not be skipped like an unknown ignorable destination. + */ + DEST_ANNOTATION, + + /* The remaining destinations hold text that we report. They must + stay contiguous and DEST_FIRST_TEXT/DEST_LAST_TEXT must bracket + them; see is_text_dest(). */ + + DEST_TITLE, + DEST_SUBJECT, + DEST_AUTHOR, + DEST_OPERATOR, + DEST_MANAGER, + DEST_COMPANY, + DEST_CATEGORY, + DEST_KEYWORDS, + DEST_DOCCOMM, + DEST_HLINKBASE, + DEST_GENERATOR, + DEST_TEMPLATE, + DEST_ATNAUTHOR, + DEST_ATNID, + DEST_PROPNAME, + DEST_STATICVAL, + DEST_ATNDATE, + + /* Date destinations; they hold `\yr', `\mo', ... rather than text. */ + + DEST_CREATIM, + DEST_REVTIM, + DEST_PRINTIM, + DEST_BUPTIM +}; + +/** + * First destination that accumulates text. + */ +#define DEST_FIRST_TEXT DEST_TITLE + +/** + * Last destination that accumulates text. + */ +#define DEST_LAST_TEXT DEST_ATNDATE + +/** + * First destination that assembles a date. + */ +#define DEST_FIRST_DATE DEST_CREATIM + + +/** + * State of one RTF group. Pushed on `{', popped on `}'. + */ +struct Group +{ + /** + * Destination in effect; inherited from the enclosing group unless + * this group introduced one of its own. + */ + enum Destination dest; + + /** + * Number of bytes an unrepresentable Unicode character is replaced + * by, from `\ucN'. Scoped like a character property. + */ + int uc; + + /** + * True if this group introduced @e dest, and hence has to flush it + * when it is closed. + */ + int owns; + + /** + * True if the content of this group is to be ignored entirely. + */ + int skip; +}; + + +/** + * State kept while extracting from one document. + */ +struct RtfContext +{ + /** + * Extraction context we were called with. + */ + struct EXTRACTOR_ExtractContext *ec; + + /** + * Set to 1 once the caller asked us to stop. + */ + int stop; + + /** + * Buffer most recently returned by the datasource. + */ + const unsigned char *rbuf; + + /** + * Number of valid bytes in @e rbuf. + */ + size_t rhave; + + /** + * Read position in @e rbuf. + */ + size_t rpos; + + /** + * Single byte pushed back, or -1. + */ + int pushback; + + /** + * Total number of bytes read so far. + */ + uint64_t consumed; + + /** + * Set once the datasource is exhausted. + */ + int eof; + + /** + * Group stack; entry 0 is the state outside of any group. + */ + struct Group stack[MAX_DEPTH]; + + /** + * Index of the innermost tracked group in @e stack. + */ + int depth; + + /** + * Number of groups nested below @e stack[MAX_DEPTH - 1] that we + * could not track individually. + */ + unsigned int extra_depth; + + /** + * Number of RTF `characters' still to be skipped because of a + * preceding `\uN'. + */ + int skip_units; + + /** + * Set by `\*'; the control word that follows introduces an ignorable + * destination. + */ + int pending_ignorable; + + /** + * UTF-8 text accumulated for the current destination. + */ + char utf8[MAX_VALUE + 1]; + + /** + * Number of bytes used in @e utf8. + */ + size_t utf8_len; + + /** + * Bytes accumulated in @e charset, not yet converted. + */ + char raw[MAX_VALUE + 1]; + + /** + * Number of bytes used in @e raw. + */ + size_t raw_len; + + /** + * Character set the `\'hh' escapes and plain bytes are in. + */ + const char *charset; + + /** + * True if @e charset was declared by the document rather than being + * our fallback. + */ + int charset_declared; + + /** + * Platform the document was written on, if the character set + * keyword gives it away, otherwise NULL. `\ansi' does not: every + * producer on every system writes that one. + */ + const char *authoring_os; + + /** + * High surrogate seen in a `\uN', waiting for its low half. + */ + unsigned int surrogate; + + /** + * Fields of the date currently being assembled, and whether we saw + * a `\yr' at all. + */ + int have_date; + int d_yr; + int d_mo; + int d_dy; + int d_hr; + int d_mi; + int d_se; + + /** + * Name of the custom document property we are inside of. + */ + char propname[256]; + + /** + * Names from the revision table, in table order. + */ + char *revauth[MAX_REVAUTHORS]; + + /** + * Number of used entries in @e revauth. + */ + unsigned int revauth_len; + + /** + * Revision marks seen in the body, to be resolved against + * @e revauth at the end. + */ + struct + { + /** + * Index into the revision table. + */ + unsigned int idx; + + /** + * Packed date and time of the edit. + */ + uint32_t dttm; + } rev[MAX_REVISIONS]; + + /** + * Number of used entries in @e rev. + */ + unsigned int rev_len; + + /** + * Author index from the most recent `\revauth' and friends, or -1. + */ + int pending_author; + + /** + * Author of the annotation we are in the middle of, if any. + */ + char atn_author[256]; + + /** + * Strings we have already reported, to suppress duplicates. + */ + char *seen[MAX_SEEN]; + + /** + * Number of used entries in @e seen. + */ + unsigned int seen_len; +}; + + +/** + * Map from a control word introducing a destination to the + * destination it introduces. + */ +static const struct +{ + const char *word; + enum Destination dest; +} dest_words[] = { + { "info", DEST_INFO }, + { "revtbl", DEST_REVTBL }, + { "userprops", DEST_USERPROPS }, + { "annotation", DEST_ANNOTATION }, + { "title", DEST_TITLE }, + { "subject", DEST_SUBJECT }, + { "author", DEST_AUTHOR }, + { "operator", DEST_OPERATOR }, + { "manager", DEST_MANAGER }, + { "company", DEST_COMPANY }, + { "category", DEST_CATEGORY }, + { "keywords", DEST_KEYWORDS }, + { "doccomm", DEST_DOCCOMM }, + { "hlinkbase", DEST_HLINKBASE }, + { "generator", DEST_GENERATOR }, + { "template", DEST_TEMPLATE }, + { "atnauthor", DEST_ATNAUTHOR }, + { "atnid", DEST_ATNID }, + { "atndate", DEST_ATNDATE }, + { "propname", DEST_PROPNAME }, + { "staticval", DEST_STATICVAL }, + { "creatim", DEST_CREATIM }, + { "revtim", DEST_REVTIM }, + { "printim", DEST_PRINTIM }, + { "buptim", DEST_BUPTIM }, + { NULL, DEST_NONE } +}; + + +/** + * Destinations that are not marked ignorable but that we still have no + * use for. Skipping them is not merely an optimisation: `\pict' and + * `\objdata' hold megabytes of hex-encoded binary. + */ +static const char *skip_words[] = { + "fonttbl", + "colortbl", + "stylesheet", + "listtable", + "listoverridetable", + "pict", + "objdata", + "objalias", + "objsect", + "objclass", + "objname", + NULL +}; + + +/** + * Map from a text destination to the meta data type to report it as. + */ +static const struct +{ + enum Destination dest; + enum EXTRACTOR_MetaType type; +} text_map[] = { + { DEST_TITLE, EXTRACTOR_METATYPE_TITLE }, + { DEST_SUBJECT, EXTRACTOR_METATYPE_SUBJECT }, + { DEST_AUTHOR, EXTRACTOR_METATYPE_AUTHOR_NAME }, + { DEST_OPERATOR, EXTRACTOR_METATYPE_LAST_SAVED_BY }, + { DEST_MANAGER, EXTRACTOR_METATYPE_MANAGER }, + { DEST_COMPANY, EXTRACTOR_METATYPE_COMPANY }, + { DEST_CATEGORY, EXTRACTOR_METATYPE_SECTION }, + { DEST_KEYWORDS, EXTRACTOR_METATYPE_KEYWORDS }, + { DEST_DOCCOMM, EXTRACTOR_METATYPE_COMMENT }, + { DEST_HLINKBASE, EXTRACTOR_METATYPE_URL }, + { DEST_GENERATOR, EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE }, + { DEST_TEMPLATE, EXTRACTOR_METATYPE_TEMPLATE }, + { DEST_ATNAUTHOR, EXTRACTOR_METATYPE_CONTRIBUTOR_NAME }, + { DEST_ATNID, EXTRACTOR_METATYPE_CONTRIBUTOR_NAME }, + { DEST_NONE, EXTRACTOR_METATYPE_UNKNOWN } +}; + + +/** + * Map from a date destination to the meta data type to report it as. + */ +static const struct +{ + enum Destination dest; + enum EXTRACTOR_MetaType type; +} date_map[] = { + { DEST_CREATIM, EXTRACTOR_METATYPE_CREATION_DATE }, + { DEST_REVTIM, EXTRACTOR_METATYPE_MODIFICATION_DATE }, + { DEST_PRINTIM, EXTRACTOR_METATYPE_LAST_PRINTED }, + { DEST_BUPTIM, EXTRACTOR_METATYPE_UNKNOWN_DATE }, + { DEST_NONE, EXTRACTOR_METATYPE_UNKNOWN } +}; + + +/** + * Custom document properties that carry personal information. Word + * writes these when a document is circulated for review from Outlook, + * and they routinely outlive the review. + */ +static const struct +{ + const char *name; + enum EXTRACTOR_MetaType type; +} custom_map[] = { + { "_AuthorEmail", EXTRACTOR_METATYPE_AUTHOR_EMAIL }, + { "_AuthorEmailDisplayName", EXTRACTOR_METATYPE_AUTHOR_NAME }, + { "_EmailSubject", EXTRACTOR_METATYPE_SUBJECT }, + { NULL, EXTRACTOR_METATYPE_UNKNOWN } +}; + + +/* ******************** generic helpers ******************** */ + + +/** + * Check whether @a d is a destination whose text we accumulate. + * + * @param d destination to check + * @return true if text in @a d is to be collected + */ +static int +is_text_dest (enum Destination d) +{ + return (DEST_FIRST_TEXT <= d) && (DEST_LAST_TEXT >= d); +} + + +/** + * Trim leading and trailing white space in @a s, in place. + * + * @param s 0-terminated string to trim + * @return pointer into @a s to the first non-blank character + */ +static char * +trim (char *s) +{ + size_t len = strlen (s); + + while ( (0 < len) && + (isspace ((unsigned char) s[len - 1])) ) + s[--len] = '\0'; + while (isspace ((unsigned char) s[0])) + s++; + return s; +} + + +/** + * Check whether @a value was reported before, and remember it if not. + * + * @param rc our extraction state + * @param value string to check + * @return 1 if @a value is new (and was remembered), 0 if it is a + * duplicate or if we ran out of space to remember it + */ +static int +mark_seen (struct RtfContext *rc, + const char *value) +{ + unsigned int i; + char *dup; + + for (i = 0; i < rc->seen_len; i++) + if (0 == strcmp (rc->seen[i], value)) + return 0; + if (MAX_SEEN == rc->seen_len) + return 0; + if (NULL == (dup = strdup (value))) + return 0; + rc->seen[rc->seen_len++] = dup; + return 1; +} + + +/** + * Check that @a s is well-formed UTF-8. + * + * We announce our values as #EXTRACTOR_METAFORMAT_UTF8, so a string + * the character set conversion could not handle has to be dropped + * rather than passed on. This happens for documents that use a code + * page they never declare: `\ansicpg' is optional, and a file written + * with a double byte character set but no `\ansicpg' cannot be decoded + * from the `\info' group alone. + * + * @param s 0-terminated string to check + * @return true if @a s is valid UTF-8 + */ +static int +is_utf8 (const char *s) +{ + const unsigned char *p = (const unsigned char *) s; + + while ('\0' != p[0]) + { + unsigned int extra; + unsigned int i; + uint32_t cp; + + if (0x80 > p[0]) + { + p++; + continue; + } + if ( (0xC2 <= p[0]) && + (0xDF >= p[0]) ) + { + extra = 1; + cp = p[0] & 0x1FU; + } + else if ( (0xE0 <= p[0]) && + (0xEF >= p[0]) ) + { + extra = 2; + cp = p[0] & 0x0FU; + } + else if ( (0xF0 <= p[0]) && + (0xF4 >= p[0]) ) + { + extra = 3; + cp = p[0] & 0x07U; + } + else + { + return 0; + } + for (i = 1; i <= extra; i++) + { + /* A premature 0-terminator fails this test as well, so we never + read beyond the end of the string. */ + if (0x80 != (p[i] & 0xC0)) + return 0; + cp = (cp << 6) | (p[i] & 0x3FU); + } + if ( ( (1 == extra) && (0x80 > cp) ) || + ( (2 == extra) && (0x800 > cp) ) || + ( (3 == extra) && (0x10000 > cp) ) || + (0x10FFFF < cp) || + ( (0xD800 <= cp) && (0xDFFF >= cp) ) ) + return 0; /* overlong, out of range or a surrogate half */ + p += extra + 1; + } + return 1; +} + + +/** + * Report a meta data value, unless it is empty or a duplicate. + * + * @param rc our extraction state + * @param type meta data type to report the value as + * @param value the value; leading and trailing white space is removed + */ +static void +add_meta (struct RtfContext *rc, + enum EXTRACTOR_MetaType type, + const char *value) +{ + char *tmp; + char *val; + + if ( (0 != rc->stop) || + (NULL == value) ) + return; + if (MAX_VALUE < strlen (value)) + return; + if (NULL == (tmp = strdup (value))) + return; + val = trim (tmp); + if ( ('\0' == val[0]) || + (0 == is_utf8 (val)) ) + { + free (tmp); + return; + } + /* De-duplicate per type: the same name legitimately shows up as both + an author and a comment author, and both are worth reporting. */ + { + char full[MAX_VALUE + 64]; + + snprintf (full, + sizeof (full), + "%d:%s", + (int) type, + val); + if (0 == mark_seen (rc, full)) + { + free (tmp); + return; + } + } + if (0 != rc->ec->proc (rc->ec->cls, + PLUGIN_NAME, + type, + EXTRACTOR_METAFORMAT_UTF8, + "text/plain", + val, + strlen (val) + 1)) + rc->stop = 1; + free (tmp); +} + + +/* ******************** reading ******************** */ + + +/** + * Obtain the next byte of the file. + * + * @param rc our extraction state + * @return the byte, or -1 at the end of the file + */ +static int +rtf_getc (struct RtfContext *rc) +{ + void *data; + ssize_t got; + + if (0 <= rc->pushback) + { + int c = rc->pushback; + + rc->pushback = -1; + return c; + } + if (rc->rpos < rc->rhave) + return rc->rbuf[rc->rpos++]; + if (0 != rc->eof) + return -1; + if (MAX_SCAN_BYTES <= rc->consumed) + { + rc->eof = 1; + return -1; + } + got = rc->ec->read (rc->ec->cls, + &data, + READ_CHUNK); + if ( (0 >= got) || + (NULL == data) ) + { + rc->eof = 1; + return -1; + } + rc->rbuf = data; + rc->rhave = (size_t) got; + rc->rpos = 0; + rc->consumed += (uint64_t) got; + return rc->rbuf[rc->rpos++]; +} + + +/** + * Push @a c back, to be returned by the next #rtf_getc(). At most one + * byte can be pushed back at a time. + * + * @param rc our extraction state + * @param c byte to push back, or -1 for none + */ +static void +rtf_ungetc (struct RtfContext *rc, + int c) +{ + if (0 <= c) + rc->pushback = c; +} + + +/* ******************** text accumulation ******************** */ + + +/** + * Convert the bytes accumulated in the current character set and + * append the result to the UTF-8 buffer. + * + * @param rc our extraction state + */ +static void +flush_raw (struct RtfContext *rc) +{ + char *conv; + size_t len; + + if (0 == rc->raw_len) + return; + conv = EXTRACTOR_common_convert_to_utf8 (rc->raw, + rc->raw_len, + rc->charset); + rc->raw_len = 0; + if (NULL == conv) + return; + len = strlen (conv); + if (len > MAX_VALUE - rc->utf8_len) + len = MAX_VALUE - rc->utf8_len; + memcpy (&rc->utf8[rc->utf8_len], + conv, + len); + rc->utf8_len += len; + rc->utf8[rc->utf8_len] = '\0'; + free (conv); +} + + +/** + * Append the byte @a b, which is in the document's character set, to + * the text of the current destination. + * + * @param rc our extraction state + * @param b byte to append + */ +static void +add_raw (struct RtfContext *rc, + unsigned char b) +{ + if (MAX_VALUE <= rc->raw_len) + flush_raw (rc); + if (MAX_VALUE <= rc->raw_len) + return; + rc->raw[rc->raw_len++] = (char) b; +} + + +/** + * Append the Unicode code point @a cp to the text of the current + * destination. + * + * @param rc our extraction state + * @param cp code point to append + */ +static void +add_codepoint (struct RtfContext *rc, + unsigned int cp) +{ + char buf[4]; + size_t n; + + flush_raw (rc); + if (0x80 > cp) + { + buf[0] = (char) cp; + n = 1; + } + else if (0x800 > cp) + { + buf[0] = (char) (0xC0 | (cp >> 6)); + buf[1] = (char) (0x80 | (cp & 0x3F)); + n = 2; + } + else if (0x10000 > cp) + { + buf[0] = (char) (0xE0 | (cp >> 12)); + buf[1] = (char) (0x80 | ((cp >> 6) & 0x3F)); + buf[2] = (char) (0x80 | (cp & 0x3F)); + n = 3; + } + else + { + buf[0] = (char) (0xF0 | (cp >> 18)); + buf[1] = (char) (0x80 | ((cp >> 12) & 0x3F)); + buf[2] = (char) (0x80 | ((cp >> 6) & 0x3F)); + buf[3] = (char) (0x80 | (cp & 0x3F)); + n = 4; + } + if (n > MAX_VALUE - rc->utf8_len) + return; + memcpy (&rc->utf8[rc->utf8_len], + buf, + n); + rc->utf8_len += n; + rc->utf8[rc->utf8_len] = '\0'; +} + + +/** + * Discard whatever text was accumulated so far. + * + * @param rc our extraction state + */ +static void +reset_text (struct RtfContext *rc) +{ + rc->utf8_len = 0; + rc->utf8[0] = '\0'; + rc->raw_len = 0; + rc->surrogate = 0; +} + + +/** + * Obtain the accumulated text of the current destination. + * + * @param rc our extraction state + * @return 0-terminated UTF-8 string owned by @a rc + */ +static const char * +get_text (struct RtfContext *rc) +{ + flush_raw (rc); + return rc->utf8; +} + + +/* ******************** dates ******************** */ + + +/** + * Format the date currently being assembled. + * + * RTF stores local time and gives no time zone, which is exactly what + * makes these stamps interesting: compared against any absolute time + * in the same document they reveal where the author sat. We therefore + * report them verbatim rather than pretending they were UTC. + * + * @param rc our extraction state + * @param buf where to write the result + * @param buf_size number of bytes available in @a buf + * @return true if a plausible date was formatted + */ +static int +format_date (struct RtfContext *rc, + char *buf, + size_t buf_size) +{ + if ( (0 == rc->have_date) || + (1600 > rc->d_yr) || + (9999 < rc->d_yr) || + (1 > rc->d_mo) || + (12 < rc->d_mo) || + (1 > rc->d_dy) || + (31 < rc->d_dy) || + (0 > rc->d_hr) || + (23 < rc->d_hr) || + (0 > rc->d_mi) || + (59 < rc->d_mi) || + (0 > rc->d_se) || + (59 < rc->d_se) ) + return 0; + return (0 < snprintf (buf, + buf_size, + "%04d-%02d-%02dT%02d:%02d:%02d", + rc->d_yr, + rc->d_mo, + rc->d_dy, + rc->d_hr, + rc->d_mi, + rc->d_se)); +} + + +/** + * Format the packed date and time @a dttm as used by the revision + * marks. Its layout is given in the RTF specification: minute in bits + * 0-5, hour in 6-10, day of month in 11-15, month in 16-19, year minus + * 1900 in 20-28 and the day of the week in 29-31. + * + * @param dttm packed value + * @param buf where to write the result + * @param buf_size number of bytes available in @a buf + * @return true if a plausible date was formatted + */ +static int +format_dttm (uint32_t dttm, + char *buf, + size_t buf_size) +{ + unsigned int mi = dttm & 0x3F; + unsigned int hr = (dttm >> 6) & 0x1F; + unsigned int dy = (dttm >> 11) & 0x1F; + unsigned int mo = (dttm >> 16) & 0x0F; + unsigned int yr = 1900 + ((dttm >> 20) & 0x1FF); + + if ( (1 > mo) || + (12 < mo) || + (1 > dy) || + (31 < dy) || + (23 < hr) || + (59 < mi) || + (1980 > yr) ) + return 0; + return (0 < snprintf (buf, + buf_size, + "%04u-%02u-%02uT%02u:%02u:00", + yr, + mo, + dy, + hr, + mi)); +} + + +/* ******************** reporting ******************** */ + + +/** + * Report that @a author worked on the document at @a date. + * + * @param rc our extraction state + * @param fmt format string taking the author and the date + * @param author name of the person + * @param date when they edited the document + */ +static void +add_revision (struct RtfContext *rc, + const char *fmt, + const char *author, + const char *date) +{ + size_t bsize = strlen (author) + strlen (date) + strlen (fmt) + 1; + char *line; + + if (MAX_VALUE < bsize) + return; + if (NULL == (line = malloc (bsize))) + return; + if (0 < snprintf (line, + bsize, + fmt, + author, + date)) + add_meta (rc, + EXTRACTOR_METATYPE_REVISION_HISTORY, + line); + free (line); +} + + +/** + * Remember an entry of the revision table and report the name. + * + * @param rc our extraction state + * @param name name of the author, may be the placeholder `Unknown' + */ +static void +add_revauthor (struct RtfContext *rc, + const char *name) +{ + char *dup; + char *val; + + if (NULL == (dup = strdup (name))) + return; + val = trim (dup); + /* Strip the terminating semicolon of the `{Author;}' subgroup. */ + { + size_t len = strlen (val); + + if ( (0 < len) && + (';' == val[len - 1]) ) + val[len - 1] = '\0'; + val = trim (val); + } + if (MAX_REVAUTHORS > rc->revauth_len) + { + char *keep; + + if (NULL != (keep = strdup (val))) + rc->revauth[rc->revauth_len++] = keep; + } + /* Entry 0 of the table is the placeholder for `no author'. */ + if (0 != strcmp (val, "Unknown")) + add_meta (rc, + EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, + val); + free (dup); +} + + +/** + * Remember a revision mark from the body, to be resolved against the + * revision table once the whole file has been read. + * + * @param rc our extraction state + * @param idx index into the revision table + * @param dttm packed date and time of the edit + */ +static void +add_revision_mark (struct RtfContext *rc, + unsigned int idx, + uint32_t dttm) +{ + unsigned int i; + + for (i = 0; i < rc->rev_len; i++) + if ( (rc->rev[i].idx == idx) && + (rc->rev[i].dttm == dttm) ) + return; + if (MAX_REVISIONS == rc->rev_len) + return; + rc->rev[rc->rev_len].idx = idx; + rc->rev[rc->rev_len].dttm = dttm; + rc->rev_len++; +} + + +/** + * Report that the author of the comment we are in the middle of wrote + * it at the packed time @a dttm. + * + * @param rc our extraction state + * @param dttm packed date and time + */ +static void +report_annotation_date (struct RtfContext *rc, + uint32_t dttm) +{ + char buf[32]; + + if ('\0' == rc->atn_author[0]) + return; + if (0 == format_dttm (dttm, buf, sizeof (buf))) + return; + add_revision (rc, + _ ("Author `%s' commented on the document on `%s'"), + rc->atn_author, + buf); +} + + +/** + * Report the value of the custom document property @a rc->propname. + * + * @param rc our extraction state + * @param value value of the property + */ +static void +report_property (struct RtfContext *rc, + const char *value) +{ + unsigned int i; + + if ('\0' == rc->propname[0]) + return; + for (i = 0; NULL != custom_map[i].name; i++) + if (0 == strcmp (custom_map[i].name, rc->propname)) + { + add_meta (rc, custom_map[i].type, value); + rc->propname[0] = '\0'; + return; + } + /* Properties we have no type for are still worth reporting: this is + where users park whatever their organisation asked them to. */ + if ('_' != rc->propname[0]) + { + char line[MAX_VALUE + 1]; + + if ( (0 < snprintf (line, + sizeof (line), + "%s: %s", + rc->propname, + value)) ) + add_meta (rc, EXTRACTOR_METATYPE_UNKNOWN, line); + } + rc->propname[0] = '\0'; +} + + +/* ******************** group handling ******************** */ + + +/** + * Handle the end of the destination @a d. + * + * @param rc our extraction state + * @param d destination that is being left + */ +static void +close_destination (struct RtfContext *rc, + enum Destination d) +{ + unsigned int i; + + if (is_text_dest (d)) + { + const char *text = get_text (rc); + + switch (d) + { + case DEST_PROPNAME: + { + char *dup = strdup (text); + + rc->propname[0] = '\0'; + if (NULL != dup) + { + char *val = trim (dup); + + if (sizeof (rc->propname) > strlen (val)) + memcpy (rc->propname, val, strlen (val) + 1); + free (dup); + } + break; + } + case DEST_STATICVAL: + report_property (rc, text); + break; + case DEST_ATNDATE: + { + /* Some producers write the time stamp of a comment as the text + of a `{\*\atndate ...}' destination rather than as the + parameter of the control word. */ + char *endp; + long long v = strtoll (text, &endp, 10); + + if ( (endp != text) && + (0 <= v) && + (0xFFFFFFFFLL >= v) ) + report_annotation_date (rc, (uint32_t) v); + break; + } + case DEST_ATNAUTHOR: + case DEST_ATNID: + { + char *dup = strdup (text); + + if (NULL != dup) + { + char *val = trim (dup); + + if ( ('\0' != val[0]) && + (sizeof (rc->atn_author) > strlen (val)) ) + memcpy (rc->atn_author, val, strlen (val) + 1); + free (dup); + } + add_meta (rc, EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, text); + break; + } + case DEST_GENERATOR: + { + /* Word terminates the generator string with a semicolon, the + way it does the entries of the font and colour tables. */ + char *dup = strdup (text); + + if (NULL != dup) + { + size_t len = strlen (dup); + + if ( (0 < len) && + (';' == dup[len - 1]) ) + dup[len - 1] = '\0'; + add_meta (rc, EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, dup); + free (dup); + } + break; + } + default: + for (i = 0; DEST_NONE != text_map[i].dest; i++) + if (text_map[i].dest == d) + { + add_meta (rc, text_map[i].type, text); + break; + } + break; + } + reset_text (rc); + return; + } + if (DEST_FIRST_DATE <= d) + { + char buf[32]; + + if (0 != format_date (rc, buf, sizeof (buf))) + for (i = 0; DEST_NONE != date_map[i].dest; i++) + if (date_map[i].dest == d) + { + add_meta (rc, date_map[i].type, buf); + break; + } + rc->have_date = 0; + return; + } +} + + +/** + * Enter a new group. + * + * @param rc our extraction state + */ +static void +push_group (struct RtfContext *rc) +{ + rc->skip_units = 0; + if (MAX_DEPTH - 1 > rc->depth) + { + rc->depth++; + rc->stack[rc->depth] = rc->stack[rc->depth - 1]; + rc->stack[rc->depth].owns = 0; + /* Each subgroup of the revision table names one author. */ + if (DEST_REVTBL == rc->stack[rc->depth].dest) + reset_text (rc); + } + else + { + rc->extra_depth++; + } +} + + +/** + * Leave the innermost group. + * + * @param rc our extraction state + */ +static void +pop_group (struct RtfContext *rc) +{ + struct Group *g; + + rc->skip_units = 0; + if (0 < rc->extra_depth) + { + rc->extra_depth--; + return; + } + if (0 == rc->depth) + return; + g = &rc->stack[rc->depth]; + if (0 == g->skip) + { + if (0 != g->owns) + close_destination (rc, g->dest); + else if (DEST_REVTBL == g->dest) + add_revauthor (rc, get_text (rc)); + } + rc->depth--; + if (DEST_REVTBL == rc->stack[rc->depth].dest) + reset_text (rc); +} + + +/* ******************** control words ******************** */ + + +/** + * Note that the document is written in the code page @a cpg. + * + * @param cpg code page number from `\ansicpg' + * @return name of the character set, NULL if unknown + */ +static const char * +codepage_to_charset (int64_t cpg) +{ + switch (cpg) + { + case 437: + return "CP437"; + case 708: + return "ISO-8859-6"; + case 850: + return "CP850"; + case 852: + return "CP852"; + case 862: + return "CP862"; + case 864: + return "CP864"; + case 866: + return "CP866"; + case 874: + return "CP874"; + case 932: + return "CP932"; + case 936: + return "CP936"; + case 949: + return "CP949"; + case 950: + return "CP950"; + case 1250: + return "CP1250"; + case 1251: + return "CP1251"; + case 1252: + return "CP1252"; + case 1253: + return "CP1253"; + case 1254: + return "CP1254"; + case 1255: + return "CP1255"; + case 1256: + return "CP1256"; + case 1257: + return "CP1257"; + case 1258: + return "CP1258"; + case 10000: + return "MACINTOSH"; + case 65001: + return "UTF-8"; + default: + return NULL; + } +} + + +/** + * Handle a control word that carries a component of the date currently + * being assembled. + * + * @param rc our extraction state + * @param word the control word, without the backslash + * @param param its numeric parameter + * @return true if @a word was a date component + */ +static int +handle_date_word (struct RtfContext *rc, + const char *word, + int64_t param) +{ + if (0 == strcmp (word, "yr")) + rc->d_yr = (int) param; + else if (0 == strcmp (word, "mo")) + rc->d_mo = (int) param; + else if (0 == strcmp (word, "dy")) + rc->d_dy = (int) param; + else if (0 == strcmp (word, "hr")) + rc->d_hr = (int) param; + else if (0 == strcmp (word, "min")) + rc->d_mi = (int) param; + else if (0 == strcmp (word, "sec")) + rc->d_se = (int) param; + else + return 0; + rc->have_date = 1; + return 1; +} + + +/** + * Handle a control word that reports a number from the `\info' group. + * + * @param rc our extraction state + * @param word the control word, without the backslash + * @param param its numeric parameter + * @return true if @a word was such a control word + */ +static int +handle_info_number (struct RtfContext *rc, + const char *word, + int64_t param) +{ + static const struct + { + const char *word; + enum EXTRACTOR_MetaType type; + } map[] = { + { "version", EXTRACTOR_METATYPE_REVISION_NUMBER }, + { "edmins", EXTRACTOR_METATYPE_TOTAL_EDITING_TIME }, + { "nofpages", EXTRACTOR_METATYPE_PAGE_COUNT }, + { "nofwords", EXTRACTOR_METATYPE_WORD_COUNT }, + { "nofchars", EXTRACTOR_METATYPE_CHARACTER_COUNT }, + { NULL, EXTRACTOR_METATYPE_UNKNOWN } + }; + unsigned int i; + + for (i = 0; NULL != map[i].word; i++) + if (0 == strcmp (map[i].word, word)) + { + char buf[32]; + + if (0 > param) + return 1; + snprintf (buf, sizeof (buf), "%llu", (unsigned long long) param); + add_meta (rc, map[i].type, buf); + return 1; + } + return 0; +} + + +/** + * Handle a control word that identifies the author of a revision, or + * the date of one. + * + * @param rc our extraction state + * @param word the control word, without the backslash + * @param param its numeric parameter + * @return true if @a word was a revision mark + */ +static int +handle_revision_word (struct RtfContext *rc, + const char *word, + int64_t param) +{ + static const char *author_words[] = { + "revauth", + "revauthdel", + "crauth", + "trauth", + "pnrauth", + NULL + }; + static const char *date_words[] = { + "revdttm", + "revdttmdel", + "crdate", + "trdate", + "pnrdate", + NULL + }; + unsigned int i; + + for (i = 0; NULL != author_words[i]; i++) + if (0 == strcmp (author_words[i], word)) + { + rc->pending_author = ( (0 <= param) && + (MAX_REVAUTHORS > param) ) + ? (int) param + : -1; + return 1; + } + for (i = 0; NULL != date_words[i]; i++) + if (0 == strcmp (date_words[i], word)) + { + if (0 <= rc->pending_author) + add_revision_mark (rc, + (unsigned int) rc->pending_author, + (uint32_t) param); + rc->pending_author = -1; + return 1; + } + return 0; +} + + +/** + * Act on the control word @a word. + * + * @param rc our extraction state + * @param word the control word, without the backslash + * @param has_param true if @a word had a numeric parameter + * @param param the numeric parameter, 0 if there was none + */ +static void +handle_word (struct RtfContext *rc, + const char *word, + int has_param, + int64_t param) +{ + struct Group *g = &rc->stack[rc->depth]; + int ignorable = rc->pending_ignorable; + unsigned int i; + + rc->pending_ignorable = 0; + if ( (0 != has_param) && + (0 == strcmp (word, "atndate")) ) + { + report_annotation_date (rc, (uint32_t) param); + return; + } + for (i = 0; NULL != dest_words[i].word; i++) + if (0 == strcmp (dest_words[i].word, word)) + { + g->dest = dest_words[i].dest; + g->owns = 1; + if (is_text_dest (g->dest)) + reset_text (rc); + if (DEST_FIRST_DATE <= g->dest) + { + rc->have_date = 0; + rc->d_yr = 0; + rc->d_mo = 0; + rc->d_dy = 0; + rc->d_hr = 0; + rc->d_mi = 0; + rc->d_se = 0; + } + return; + } + if (0 != ignorable) + { + /* An ignorable destination we do not know: the specification says + to discard it, which is what keeps us out of `\*\datastore', + `\*\latentstyles', `\*\shppict' and the like. */ + g->dest = DEST_SKIP; + g->owns = 0; + g->skip = 1; + return; + } + for (i = 0; NULL != skip_words[i]; i++) + if (0 == strcmp (skip_words[i], word)) + { + g->dest = DEST_SKIP; + g->owns = 0; + g->skip = 1; + return; + } + if (0 == strcmp (word, "uc")) + { + if ( (0 != has_param) && + (0 <= param) && + (MAX_VALUE > param) ) + g->uc = (int) param; + return; + } + if (0 == strcmp (word, "ansicpg")) + { + const char *cs = codepage_to_charset (param); + + if (NULL != cs) + { + rc->charset = cs; + rc->charset_declared = 1; + } + return; + } + if (0 == strcmp (word, "mac")) + { + rc->charset = "MACINTOSH"; + rc->charset_declared = 1; + rc->authoring_os = "Macintosh"; + return; + } + if (0 == strcmp (word, "pc")) + { + rc->charset = "CP437"; + rc->charset_declared = 1; + rc->authoring_os = "MS-DOS"; + return; + } + if (0 == strcmp (word, "pca")) + { + rc->charset = "CP850"; + rc->charset_declared = 1; + rc->authoring_os = "MS-DOS"; + return; + } + if (DEST_FIRST_DATE <= g->dest) + { + if (0 != handle_date_word (rc, word, param)) + return; + } + if (DEST_INFO == g->dest) + { + if (0 != handle_info_number (rc, word, param)) + return; + } + (void) handle_revision_word (rc, word, param); +} + + +/** + * Act on a `\uN' Unicode escape. + * + * @param rc our extraction state + * @param param the code point, as a signed 16 bit number + */ +static void +handle_unicode (struct RtfContext *rc, + int64_t param) +{ + unsigned int cp; + + if (0 > param) + param += 0x10000; + if ( (0 > param) || + (0x10FFFF < param) ) + { + rc->surrogate = 0; + return; + } + cp = (unsigned int) param; + if ( (0xD800 <= cp) && + (0xDBFF >= cp) ) + { + /* Word splits characters beyond the basic multilingual plane into + a surrogate pair of two `\uN'; hold the first half. */ + rc->surrogate = cp; + return; + } + if ( (0xDC00 <= cp) && + (0xDFFF >= cp) ) + { + if (0 != rc->surrogate) + { + unsigned int full = 0x10000 + + ((rc->surrogate - 0xD800) << 10) + + (cp - 0xDC00); + + rc->surrogate = 0; + add_codepoint (rc, full); + } + /* An unpaired surrogate has no UTF-8 encoding; drop it. */ + return; + } + rc->surrogate = 0; + add_codepoint (rc, cp); +} + + +/** + * Parse the control word or control symbol that follows a backslash. + * + * @param rc our extraction state + * @param[out] word set to the control word, or to the single character + * of a control symbol + * @param[out] has_param set to true if a numeric parameter followed + * @param[out] param set to the numeric parameter + * @return 1 for a control word, 0 for a control symbol, -1 at the end + * of the file + */ +static int +parse_control (struct RtfContext *rc, + char *word, + int *has_param, + int64_t *param) +{ + int c = rtf_getc (rc); + size_t n = 0; + + word[0] = '\0'; + *has_param = 0; + *param = 0; + if (0 > c) + return -1; + if (0 == isalpha ((unsigned char) c)) + { + word[0] = (char) c; + word[1] = '\0'; + return 0; + } + while ( (0 <= c) && + (0 != isalpha ((unsigned char) c)) ) + { + if (MAX_CW > n) + word[n++] = (char) c; + c = rtf_getc (rc); + } + word[n] = '\0'; + if ( (0 <= c) && + ( ('-' == c) || + (0 != isdigit ((unsigned char) c)) ) ) + { + int neg = ('-' == c); + int64_t v = 0; + unsigned int digits = 0; + + if (0 != neg) + c = rtf_getc (rc); + while ( (0 <= c) && + (0 != isdigit ((unsigned char) c)) ) + { + if (10 > digits) + { + v = v * 10 + (c - '0'); + digits++; + } + c = rtf_getc (rc); + } + if (0 < digits) + { + *has_param = 1; + *param = (0 != neg) ? -v : v; + } + } + /* A single space after a control word is its delimiter and is not + part of the document text. */ + if (' ' != c) + rtf_ungetc (rc, c); + return 1; +} + + +/** + * Read and discard the next @a n bytes; used for the binary data that + * follows `\binN'. + * + * @param rc our extraction state + * @param n number of bytes to discard + */ +static void +skip_bytes (struct RtfContext *rc, + int64_t n) +{ + while (0 < n) + { + if (0 > rtf_getc (rc)) + return; + n--; + } +} + + +/** + * Act on a control symbol, that is a backslash followed by a character + * that is not a letter. + * + * @param rc our extraction state + * @param sym the character + * @param collect true if we are inside a destination whose text we + * accumulate + */ +static void +handle_symbol (struct RtfContext *rc, + char sym, + int collect) +{ + switch (sym) + { + case '*': + rc->pending_ignorable = 1; + break; + case '\\': + case '{': + case '}': + if (0 != collect) + add_raw (rc, (unsigned char) sym); + break; + case '\'': + { + int hi = rtf_getc (rc); + int lo; + + if ( (0 > hi) || + (0 == isxdigit ((unsigned char) hi)) ) + { + rtf_ungetc (rc, hi); + break; + } + lo = rtf_getc (rc); + if ( (0 > lo) || + (0 == isxdigit ((unsigned char) lo)) ) + { + rtf_ungetc (rc, lo); + break; + } + if (0 != collect) + { + int v = 0; + + v = (0 != isdigit ((unsigned char) hi)) + ? (hi - '0') + : (tolower ((unsigned char) hi) - 'a' + 10); + v <<= 4; + v |= (0 != isdigit ((unsigned char) lo)) + ? (lo - '0') + : (tolower ((unsigned char) lo) - 'a' + 10); + add_raw (rc, (unsigned char) v); + } + break; + } + case '~': + if (0 != collect) + add_codepoint (rc, 0x00A0); + break; + case '_': + if (0 != collect) + add_raw (rc, (unsigned char) '-'); + break; + case '\r': + case '\n': + if (0 != collect) + add_raw (rc, (unsigned char) ' '); + break; + default: + /* `\-' (optional hyphen), `\:' (subentry) and anything else we do + not know produce no text. */ + break; + } +} + + +/* ******************** main loop ******************** */ + + +/** + * Report everything we learned from the revision marks, now that the + * revision table is complete. + * + * @param rc our extraction state + */ +static void +report_revisions (struct RtfContext *rc) +{ + unsigned int i; + + for (i = 0; i < rc->rev_len; i++) + { + char buf[32]; + const char *name; + + if (rc->rev[i].idx >= rc->revauth_len) + continue; + name = rc->revauth[rc->rev[i].idx]; + if ( (NULL == name) || + ('\0' == name[0]) || + (0 == strcmp (name, "Unknown")) ) + continue; + if (0 == format_dttm (rc->rev[i].dttm, buf, sizeof (buf))) + continue; + add_revision (rc, + _ ("Author `%s' edited the document on `%s'"), + name, + buf); + } +} + + +/** + * Release everything @a rc holds on to. + * + * @param rc our extraction state + */ +static void +cleanup (struct RtfContext *rc) +{ + unsigned int i; + + for (i = 0; i < rc->seen_len; i++) + free (rc->seen[i]); + for (i = 0; i < rc->revauth_len; i++) + free (rc->revauth[i]); +} + + +/** + * Main entry method for the `rtf' extraction plugin. + * + * @param ec extraction context provided to the plugin + */ +void +EXTRACTOR_rtf_extract_method (struct EXTRACTOR_ExtractContext *ec) +{ + struct RtfContext *rc; + void *data; + + if (5 > ec->read (ec->cls, &data, 5)) + return; + if (0 != memcmp (data, "{\\rtf", 5)) + return; + if (0 != ec->seek (ec->cls, 0, SEEK_SET)) + return; + if (NULL == (rc = malloc (sizeof (struct RtfContext)))) + return; + memset (rc, 0, sizeof (struct RtfContext)); + rc->ec = ec; + rc->pushback = -1; + rc->charset = "CP1252"; + rc->pending_author = -1; + rc->stack[0].uc = 1; + if (0 != ec->proc (ec->cls, + PLUGIN_NAME, + EXTRACTOR_METATYPE_MIMETYPE, + EXTRACTOR_METAFORMAT_UTF8, + "text/plain", + "text/rtf", + strlen ("text/rtf") + 1)) + { + free (rc); + return; + } + while (0 == rc->stop) + { + int c = rtf_getc (rc); + int collect; + + if (0 > c) + break; + if ( ('{' == c) || + ('}' == c) ) + { + /* A group boundary always ends the run of characters skipped + because of a `\uN'. */ + if ('{' == c) + push_group (rc); + else + pop_group (rc); + continue; + } + collect = ( (0 == rc->stack[rc->depth].skip) && + ( (is_text_dest (rc->stack[rc->depth].dest)) || + (DEST_REVTBL == rc->stack[rc->depth].dest) ) ); + if ('\\' != c) + { + if ( ('\r' == c) || + ('\n' == c) ) + continue; /* not part of the document text */ + if (0 < rc->skip_units) + { + rc->skip_units--; + continue; + } + if (0 != collect) + add_raw (rc, (unsigned char) c); + continue; + } + { + char word[MAX_CW + 1]; + int has_param; + int64_t param; + int kind = parse_control (rc, word, &has_param, &param); + + if (0 > kind) + break; + if (0 < rc->skip_units) + { + /* A control word or symbol counts as one of the characters + that `\uN' asked us to skip; a `\'hh' takes its two hex + digits with it. */ + rc->skip_units--; + if ( (0 == kind) && + ('\'' == word[0]) ) + { + (void) rtf_getc (rc); + (void) rtf_getc (rc); + } + continue; + } + if (0 == kind) + { + handle_symbol (rc, word[0], collect); + continue; + } + if (0 == strcmp (word, "bin")) + { + if ( (0 != has_param) && + (0 < param) ) + skip_bytes (rc, param); + continue; + } + if (0 == strcmp (word, "u")) + { + if (0 != has_param) + { + if (0 != collect) + handle_unicode (rc, param); + rc->skip_units = rc->stack[rc->depth].uc; + } + continue; + } + if (0 != rc->stack[rc->depth].skip) + { + /* Still track `\*' so that a nested unknown destination inside + a skipped one does not confuse us. */ + rc->pending_ignorable = 0; + continue; + } + handle_word (rc, word, has_param, param); + } + } + if (0 != rc->charset_declared) + add_meta (rc, EXTRACTOR_METATYPE_CHARACTER_SET, rc->charset); + if (NULL != rc->authoring_os) + add_meta (rc, EXTRACTOR_METATYPE_AUTHORING_OS, rc->authoring_os); + report_revisions (rc); + cleanup (rc); + free (rc); +} + + +/* end of rtf_extractor.c */ diff --git a/src/plugins/test_rtf.c b/src/plugins/test_rtf.c @@ -0,0 +1,234 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Vidyut Samanta and Christian Grothoff + + libextractor is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; either version 3, or (at your + option) any later version. + + libextractor is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file plugins/test_rtf.c + * @brief testcase for the rtf plugin + * @author Christian Grothoff + * + * See testdata/README for where the sample documents come from and + * under what terms they are redistributed here. + */ +#include "platform.h" +#include "test_lib.h" + + +/** + * Shorthand for a UTF-8 text solution entry. + * + * @param t meta data type to expect + * @param s string value to expect + */ +#define TXT(t, s) { t, EXTRACTOR_METAFORMAT_UTF8, "text/plain", s, \ + strlen (s) + 1, 0 } + + +/** + * Main function for the rtf testcase. + * + * @param argc number of arguments (ignored) + * @param argv arguments (ignored) + * @return 0 on success + */ +int +main (int argc, char *argv[]) +{ + /* A memorandum from the National Institute of Neurological Disorders + and Stroke. The account that wrote it is a role account rather + than a person, but the document still records which one, when it + was written, when it was last saved and how long somebody spent + editing it. */ + struct SolutionData rtf_nih_sol[] = { + TXT (EXTRACTOR_METATYPE_MIMETYPE, "text/rtf"), + TXT (EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, "Microsoft Word 11.0.6568"), + TXT (EXTRACTOR_METATYPE_AUTHOR_NAME, "NINDS"), + TXT (EXTRACTOR_METATYPE_LAST_SAVED_BY, "NINDS"), + TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2006-09-29T12:02:00"), + TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2006-09-29T12:30:00"), + TXT (EXTRACTOR_METATYPE_REVISION_NUMBER, "3"), + TXT (EXTRACTOR_METATYPE_TOTAL_EDITING_TIME, "28"), + TXT (EXTRACTOR_METATYPE_PAGE_COUNT, "1"), + TXT (EXTRACTOR_METATYPE_WORD_COUNT, "170"), + TXT (EXTRACTOR_METATYPE_CHARACTER_COUNT, "973"), + TXT (EXTRACTOR_METATYPE_COMPANY, "NIH"), + TXT (EXTRACTOR_METATYPE_CHARACTER_SET, "CP1252"), + { 0, 0, NULL, NULL, 0, -1 } + }; + + /* An FDA advisory committee announcement. Same shape, and the same + `CDER USER' role account that the OLE2 PowerPoint sample uses -- + see test_ole2.c. */ + struct SolutionData rtf_fda_sol[] = { + TXT (EXTRACTOR_METATYPE_MIMETYPE, "text/rtf"), + TXT (EXTRACTOR_METATYPE_TITLE, + "MEDICAL IMAGING DRUGS ADVISORY COMMITTEE (MIDAC)"), + TXT (EXTRACTOR_METATYPE_AUTHOR_NAME, "CDER USER"), + TXT (EXTRACTOR_METATYPE_LAST_SAVED_BY, "CDER USER"), + TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2000-04-21T11:08:00"), + TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2000-07-06T12:40:00"), + TXT (EXTRACTOR_METATYPE_REVISION_NUMBER, "5"), + TXT (EXTRACTOR_METATYPE_TOTAL_EDITING_TIME, "14"), + TXT (EXTRACTOR_METATYPE_PAGE_COUNT, "2"), + TXT (EXTRACTOR_METATYPE_WORD_COUNT, "320"), + TXT (EXTRACTOR_METATYPE_CHARACTER_COUNT, "1829"), + TXT (EXTRACTOR_METATYPE_COMPANY, "FDA.CDER"), + TXT (EXTRACTOR_METATYPE_CHARACTER_SET, "CP1252"), + { 0, 0, NULL, NULL, 0, -1 } + }; + + /* A NASA Goddard document. This one shows why `\author' and + `\operator' are worth reporting separately: the file was created + under a civil servant's login and last saved by the contractor + that ran the office. */ + struct SolutionData rtf_nasa_sol[] = { + TXT (EXTRACTOR_METATYPE_MIMETYPE, "text/rtf"), + TXT (EXTRACTOR_METATYPE_AUTHOR_NAME, "WLBEHNKE"), + TXT (EXTRACTOR_METATYPE_LAST_SAVED_BY, "INTELLISOURCE"), + TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2000-02-11T07:11:00"), + TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2001-05-30T07:26:00"), + TXT (EXTRACTOR_METATYPE_REVISION_NUMBER, "6"), + TXT (EXTRACTOR_METATYPE_TOTAL_EDITING_TIME, "12"), + TXT (EXTRACTOR_METATYPE_PAGE_COUNT, "1"), + TXT (EXTRACTOR_METATYPE_WORD_COUNT, "158"), + TXT (EXTRACTOR_METATYPE_CHARACTER_COUNT, "905"), + TXT (EXTRACTOR_METATYPE_COMPANY, "GSFC/NASA"), + TXT (EXTRACTOR_METATYPE_CHARACTER_SET, "CP1252"), + { 0, 0, NULL, NULL, 0, -1 } + }; + + /* A Federal Transit Administration document produced by scanning + software, which left both its own name and a custom document + property behind. */ + struct SolutionData rtf_fta_sol[] = { + TXT (EXTRACTOR_METATYPE_MIMETYPE, "text/rtf"), + TXT (EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, "Microsoft Word 10.0.6764"), + TXT (EXTRACTOR_METATYPE_TITLE, "OCR Document"), + TXT (EXTRACTOR_METATYPE_AUTHOR_NAME, "Readiris"), + TXT (EXTRACTOR_METATYPE_LAST_SAVED_BY, "FTA"), + TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2005-09-30T12:48:00"), + TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2005-09-30T13:18:00"), + TXT (EXTRACTOR_METATYPE_REVISION_NUMBER, "3"), + TXT (EXTRACTOR_METATYPE_TOTAL_EDITING_TIME, "3"), + TXT (EXTRACTOR_METATYPE_PAGE_COUNT, "1"), + TXT (EXTRACTOR_METATYPE_WORD_COUNT, "285"), + TXT (EXTRACTOR_METATYPE_CHARACTER_COUNT, "1625"), + TXT (EXTRACTOR_METATYPE_COMPANY, "I.R.I.S."), + TXT (EXTRACTOR_METATYPE_UNKNOWN, "Editor: Readiris"), + TXT (EXTRACTOR_METATYPE_CHARACTER_SET, "CP1252"), + { 0, 0, NULL, NULL, 0, -1 } + }; + + /* Written with Word for the Macintosh, which says so by declaring + the Mac OS Roman character set. The apostrophe in the title is + `\'d5', which is U+2019 in that character set but would be a + capital O with tilde if we assumed the Windows default. */ + struct SolutionData rtf_doe_mac_sol[] = { + TXT (EXTRACTOR_METATYPE_MIMETYPE, "text/rtf"), + TXT (EXTRACTOR_METATYPE_TITLE, + "Review of the Yucca Mountain Igneous Consequences Peer Review" + " Panel\u2019s Interim Report presented August 23, 2002"), + TXT (EXTRACTOR_METATYPE_AUTHOR_NAME, "berts"), + TXT (EXTRACTOR_METATYPE_LAST_SAVED_BY, "berts"), + TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2002-09-19T11:39:00"), + TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2002-09-21T16:31:00"), + TXT (EXTRACTOR_METATYPE_REVISION_NUMBER, "8"), + TXT (EXTRACTOR_METATYPE_TOTAL_EDITING_TIME, "94"), + TXT (EXTRACTOR_METATYPE_PAGE_COUNT, "2"), + TXT (EXTRACTOR_METATYPE_WORD_COUNT, "1070"), + TXT (EXTRACTOR_METATYPE_CHARACTER_COUNT, "6100"), + TXT (EXTRACTOR_METATYPE_CHARACTER_SET, "MACINTOSH"), + TXT (EXTRACTOR_METATYPE_AUTHORING_OS, "Macintosh"), + { 0, 0, NULL, NULL, 0, -1 } + }; + + /* Synthetic, so that the paths that carry personal data can be + tested without republishing anybody's. Covers the revision table, + the three flavours of revision mark, comment authors and dates, + the Outlook review properties, a template path, `\uN' and `\'hh' + escapes. */ + struct SolutionData rtf_review_sol[] = { + TXT (EXTRACTOR_METATYPE_MIMETYPE, "text/rtf"), + TXT (EXTRACTOR_METATYPE_CREATED_BY_SOFTWARE, + "libextractor test fixture 1.0"), + TXT (EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, "Records Management Office"), + TXT (EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, + "Office of the Inspector General"), + TXT (EXTRACTOR_METATYPE_CONTRIBUTOR_NAME, "RMO"), + TXT (EXTRACTOR_METATYPE_TITLE, "Quarterly Compliance Review \u2014 2019"), + TXT (EXTRACTOR_METATYPE_SUBJECT, "Records management"), + TXT (EXTRACTOR_METATYPE_AUTHOR_NAME, "Records Management Office"), + TXT (EXTRACTOR_METATYPE_LAST_SAVED_BY, + "Office of the Inspector General"), + TXT (EXTRACTOR_METATYPE_COMPANY, "Example Agency \u2013 Region 5"), + TXT (EXTRACTOR_METATYPE_MANAGER, "Division Chief"), + TXT (EXTRACTOR_METATYPE_SECTION, "Internal"), + TXT (EXTRACTOR_METATYPE_KEYWORDS, "compliance, records, retention"), + TXT (EXTRACTOR_METATYPE_COMMENT, "Draft circulated for comment"), + TXT (EXTRACTOR_METATYPE_URL, "http://www.example.org/records/"), + TXT (EXTRACTOR_METATYPE_CREATION_DATE, "2019-03-04T09:15:00"), + TXT (EXTRACTOR_METATYPE_MODIFICATION_DATE, "2019-03-11T16:48:00"), + TXT (EXTRACTOR_METATYPE_LAST_PRINTED, "2019-03-12T08:05:00"), + TXT (EXTRACTOR_METATYPE_REVISION_NUMBER, "4"), + TXT (EXTRACTOR_METATYPE_TOTAL_EDITING_TIME, "37"), + TXT (EXTRACTOR_METATYPE_PAGE_COUNT, "2"), + TXT (EXTRACTOR_METATYPE_WORD_COUNT, "480"), + TXT (EXTRACTOR_METATYPE_CHARACTER_COUNT, "2650"), + TXT (EXTRACTOR_METATYPE_AUTHOR_EMAIL, "records@example.org"), + TXT (EXTRACTOR_METATYPE_SUBJECT, "Please review by Friday"), + TXT (EXTRACTOR_METATYPE_UNKNOWN, "Retention Schedule: GRS 4.1"), + TXT (EXTRACTOR_METATYPE_TEMPLATE, + "C:\\Program Files\\Example Agency\\Templates\\Memorandum.dot"), + TXT (EXTRACTOR_METATYPE_CHARACTER_SET, "CP1252"), + TXT (EXTRACTOR_METATYPE_REVISION_HISTORY, + "Author `Records Management Office' commented on the document on" + " `2019-03-11T16:48:00'"), + TXT (EXTRACTOR_METATYPE_REVISION_HISTORY, + "Author `Records Management Office' edited the document on" + " `2019-03-05T14:32:00'"), + TXT (EXTRACTOR_METATYPE_REVISION_HISTORY, + "Author `Office of the Inspector General' edited the document on" + " `2019-03-08T09:07:00'"), + TXT (EXTRACTOR_METATYPE_REVISION_HISTORY, + "Author `Records Management Office' edited the document on" + " `2019-03-11T16:48:00'"), + { 0, 0, NULL, NULL, 0, -1 } + }; + + struct ProblemSet ps[] = { + { "testdata/rtf_nih.rtf", + rtf_nih_sol }, + { "testdata/rtf_fda.rtf", + rtf_fda_sol }, + { "testdata/rtf_nasa.rtf", + rtf_nasa_sol }, + { "testdata/rtf_fta.rtf", + rtf_fta_sol }, +#if HAVE_ICONV + { "testdata/rtf_doe_mac.rtf", + rtf_doe_mac_sol }, +#endif + { "testdata/rtf_review.rtf", + rtf_review_sol }, + { NULL, NULL } + }; + return ET_main ("rtf", ps); +} + + +/* end of test_rtf.c */ diff --git a/src/plugins/testdata/README b/src/plugins/testdata/README @@ -100,3 +100,71 @@ licence, and every name and address in them is fictitious `persons' part, and a shared workbook revision log naming who changed the workbook and when. + + +Provenance of the RTF samples +============================= + +The files below were added for the `rtf' plugin, which extracts the +same class of information from Rich Text Format documents: who wrote +them, who last saved them, who made each tracked change and when. +They come from the same Govdocs1 corpus described above, under the +same terms, and were selected under the same criterion -- they name +organisations, role accounts or login names rather than identifiable +private individuals. + + rtf_nih.rtf govdocs1 003332.rtf + National Institute of Neurological + Disorders and Stroke. `\author' and + `\operator' are both the role account + "NINDS", `\*\company' is "NIH". + + rtf_fda.rtf govdocs1 942656.rtf + Announcement of a meeting of the Medical + Imaging Drugs Advisory Committee. Written + by the same "CDER USER" account as the + PowerPoint sample above. + + rtf_nasa.rtf govdocs1 806272.rtf + Goddard Space Flight Center. Created + under the login "WLBEHNKE" but last saved + by "INTELLISOURCE", the contractor running + the office -- which is why `\author' and + `\operator' are reported separately. + + rtf_fta.rtf govdocs1 882751.rtf + Federal Transit Administration document + produced by scanning software, which left + its own name in `\author' and a custom + document property in `\*\userprops'. + + rtf_doe_mac.rtf govdocs1 534437.rtf + Review of the Yucca Mountain Igneous + Consequences Peer Review Panel report. + Written with Word for the Macintosh, which + says so by declaring the Mac OS Roman + character set; the apostrophe in the title + is encoded as `\'d5', which decodes + correctly only if that declaration is + honoured. + +Synthetic file +-------------- + + rtf_review.rtf Written by hand for this test suite, and + hence part of libextractor and covered by + its licence. Every name and address in it + is fictitious (example.org is reserved for + documentation by RFC 2606). It covers the + paths that carry personal data and that + therefore should not be exercised with a + real document: a `\*\revtbl' revision + table with two authors, the `\revauth', + `\revauthdel' and `\crauth' flavours of + revision mark with their packed time + stamps, `\*\atnid' / `\*\atnauthor' / + `\*\atndate' for a comment, the Outlook + `_AuthorEmail' review properties, a + `\*\template' path from the authoring + machine, and both the `\uN' and the + `\'hh' character escapes. diff --git a/src/plugins/testdata/rtf_doe_mac.rtf b/src/plugins/testdata/rtf_doe_mac.rtf @@ -0,0 +1 @@ +{\rtf1\mac\ansicpg10000\uc1 \deff4\deflang1033\deflangfe1033{\upr{\fonttbl{\f0\fnil\fcharset256\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f4\fnil\fcharset256\fprq2{\*\panose 02000500000000000000}Times;}}{\*\ud{\fonttbl{\f0\fnil\fcharset256\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f4\fnil\fcharset256\fprq2{\*\panose 02000500000000000000}Times;}}}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}{\stylesheet{\widctlpar\adjustright \f4\cgrid \snext0 Normal;}{\*\cs10 \additive Default Paragraph Font;}{\s15\qc\widctlpar\adjustright \f4\cgrid \sbasedon0 \snext15 Body Text;}{\s16\qj\widctlpar\adjustright \f4\cgrid \sbasedon0 \snext16 Body Text 2;}}{\info{\title Review of the Yucca Mountain Igneous Consequences Peer Review Panel\'d5s Interim Report presented August 23, 2002}{\author berts}{\operator berts}{\creatim\yr2002\mo9\dy19\hr11\min39}{\revtim\yr2002\mo9\dy21\hr16\min31}{\version8}{\edmins94}{\nofpages2}{\nofwords1070}{\nofchars6100}{\nofcharsws7491}{\vern115}}\margt540 \widowctrl\ftnbj\aenddoc\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\hyphcaps0\formshade\viewkind1\viewscale125\pgbrdrhead\pgbrdrfoot \fet0\sectd \linex0\endnhere\sectdefaultcl {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang{\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang{\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang{\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang{\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}\pard\plain \s15\qc\widctlpar\adjustright \f4\cgrid {Review of the Yucca Mountain Igneous Consequences Peer Review Panel\rquote s Interim Report presented on August 23, 2002.\par }\pard\plain \qc\widctlpar\adjustright \f4\cgrid {\par Meghan M. Morrissey, Colorado School of Mines\par }\pard \widctlpar\adjustright {\par }\pard \qj\widctlpar\adjustright {The YMICPR Panel presented a comprehensive review on work that has been completed by DOE related to consequences of igneous events that might impact the Yucca Mountain repository. The Panel consists of a group of researchers well diverse in studies related to igneous activity however they did acknowledge their limitations in several critical areas such as groundwater transport, remobilization of tephra and the interaction between magma and waste packages. In addition to analyzing previous work, the Panel conducted their own analyses to support their recommended tasks in areas that they concluded were weak in DOE studies.\par }\pard\plain \s16\qj\fi720\widctlpar\adjustright \f4\cgrid {The Panel approached their review by beginning with magma transport and mechanical properties, followed by dike propagation, dike interaction with repository, and magma interaction with a drift(s), and ending with the consequences of the break through of magma at the surface. The review of magma transport and mechanical properties was thorough emphasizing that these properties are well constrained from thousands of data collected from volcanoes throughout the world and from established theoretical and empirical calculations. They demonstrated how the volume and general composition of magma that might erupt at Yucca Mountain Repository are constrained from field studies on volcanism within the region. Also mentioned was the sensitivity of magma flow behavior to dissolved and exsolved volatiles such as H}{\sub 2}{O and CO}{\sub 2}{. The presence of H}{\sub 2}{O enhances the solubility of CO}{\sub 2}{, which will affect the depth of exsolution and fragmentation. This means that the total volatile content of the magma will be greater as when it nears the surface. \par The consequence of how the magma will interact with the drift if intersected by a dike, strongly depends on the nature of the magma near the top of the dike. When magma is within upper 500 m of the crust it will release or exsolve most of its dissolved H}{\sub 2}{O and CO}{\sub 2}{ which can change its flow character from a viscous liquid to a bubbly magma to a gas rich mixture of fragmented magma and gas depending on the volatile content. Prior to the Panels work, the value for estimated H}{\sub 2}{O contained in Crater Flat, CF, basalts prior to eruption was 1-3 wt.% based on a literature search of similar basalts worldwide. The Panel estimated 2.5-4.0 wt.% H}{\sub 2}{O in CF basalts from thermodynamic calculations based on the crystallization sequence. This demonstrated the lack of analysis that has been done and the type of analyses that need to be done to better constrain the volatile contents of magmas erupted at CF. The Panel recommended the examination of glass and fluid inclusions in phenocrysts from the Quaternary potassic trachybasalts to estimate CO}{\sub 2}{ and to utilize up-to-date thermodynamic solubility models for CO}{\sub 2}{, S, and H}{\sub 2}{0 to better constrain the depth of exsolution and fragmentation. This and other recommended analyses mentioned in the interim report on magma transport and mechanical properties should be considered in future DOE tasks. \par The majority of the Panels presentation focused on dike propagation. The models presented involved the upward migration of a viscous magma and the results demonstrated how the crack tip might respond to the stress regime around the repository over time. The part of their presentation that appeared to be the weakest was their analysis of the consequences of the dike intersecting a drift. Existing dike propagation models are limited to 2-dimensions and viscous, incompressible magma flow. In the case of a dike intersecting a drift, the model needs to consider a 3-dimensional flow to account for the diversion of magma into the drift and its effect of the propagation of the crack tip. This model also needs to consider the range of magma flow types given that basaltic magma could contain up to 4 wt.% H}{\sub 2}{O. Under these conditions, magma behind the crack tip will likely be a bubbly magma that will respond very differently than a liquid magma without many bubbles (with a low void fraction) when it enters the drift. A viscous magma is an incompressible fluid and does not change in volume in response to a change in pressure as would be the conditions upon entering the drift or near the crack tip. A bubbly fluid is compressible given the presence of gas bubbles and will respond to a change in pressure by expanding or compressing. For example, in the case of entering the drift or during the propagation of the crack tip, a bubbly magma will experience a decrease in pressure which will cause exsolution of volatiles which in turn will change the its transport and mechanical properties\endash factors that are not considered in current dike propagation model or drift flow model. \par When modeling the dynamic behavior of the magma inside a drift, the initial pressure gradient becomes important as demonstrated by the Woods et al. model. A bubbly magma may expand and freeze or produce a Strombolian type of eruption inside the drift that would lead to rapid pressurization of the drift a scenario that has not been considered. No crack propagation flow model currently exists that consider a compressible bubbly flow regime along with an appropriate viscosity and solubility model. It is an area of volcanology that has mostly been studied by laboratory experiments and overly simplified numerical models. One approach to developing this type of model would be to couple a dike propagation model for a bubbly fluid and couple it to an appropriate flow model for inside the drift using the CFSLIB computer code by Gaffney. The drift model needs to include the elastic properties of the wall rock to predict failure conditions and shed new light on their \ldblquote dog-leg\rdblquote scenario. Given the complexity of the flow phenomenon, laboratory experiments are essential to understand the physics and to validate the numerical model. These were issues/ideas that were discussed between myself, Alan Rubin and Anthony Pearson after the Panel\rquote s presentation and should be included in their final report. \par The last topic presented was the consequences of magma breaking through to the surface. Like the other topics, there are many scenarios that influence the related calculations, in this case it the TSPA calculations for eruptive doses. The Panel recommended that more studies be conducted on how the waste packages would respond not only to slow moving, low void fraction magma but also to the other type of flow regimes especially a high-speed, high-density gas-ash flow or fast moving bubbly magma. It was brought to the attention of the Panel that lava flows can carry objects that are much heavier than those predicted by theory and that laboratory or field experiments are needed to improve models on how waste packages may become mobilized. Another concern about the TSPA calculations was the neglect of secondary transportation of contaminants by wind after deposition and fragmentation of waste material from the canisters if exposed during an eruption. \par The Panel\rquote s review raised many new issues as mentioned above and in the presentation and interim report. In the final report, the Panel needs to prioritize these issues and elaborate on how they should be addressed in future DOE tasks.\par }} +\ No newline at end of file diff --git a/src/plugins/testdata/rtf_fda.rtf b/src/plugins/testdata/rtf_fda.rtf @@ -0,0 +1,111 @@ +{\rtf1\ansi\ansicpg1252\uc1 \deff0\deflang1033\deflangfe1033{\fonttbl{\f0\froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;} +{\f2\fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New;}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0; +\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}{\stylesheet{\widctlpar\adjustright +\fs20\cgrid \snext0 Normal;}{\*\cs10 \additive Default Paragraph Font;}{\s15\widctlpar\adjustright \f1 \snext15 Style0;}}{\info{\title MEDICAL IMAGING DRUGS ADVISORY COMMITTEE (MIDAC)}{\author CDER USER}{\operator CDER USER} +{\creatim\yr2000\mo4\dy21\hr11\min8}{\revtim\yr2000\mo7\dy6\hr12\min40}{\version5}{\edmins14}{\nofpages2}{\nofwords320}{\nofchars1829}{\*\company FDA.CDER}{\nofcharsws0}{\vern113}}\margl1440\margr1440 +\widowctrl\ftnbj\aenddoc\hyphcaps0\formshade\viewkind1\viewscale100\pgbrdrhead\pgbrdrfoot \fet0\sectd \linex0\endnhere\sectdefaultcl {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang{\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang{\pntxta .}} +{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang{\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang{\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang{\pntxtb (} +{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang{\pntxtb (}{\pntxta )}}\pard\plain \s15\qc\widctlpar +\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\adjustright \f1 {\b\f2\fs20\cf1 MEDICAL IMAGING DRUGS ADVISORY COMMITTEE (MIDAC) +\par CENTER FOR DRUG EVALUATION AND RESEARCH +\par +\par \sect }\sectd \marglsxn1800\margrsxn1800\sbknone\linex0\cols2\endnhere\sectdefaultcl \pard\plain \s15\widctlpar\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\adjustright \f1 {\b\fs16\ul\cf1 CHAIRPERSON +\par +\par }{\fs16\cf1 Ponto, Laura L. Boles, Ph.D. 6/30/01 +\par Associate Research Scientist +\par P.E.T. Imaging Center +\par Department of Radiology +\par University of Iowa Hospitals and Clinics +\par 200 Hawkins Drive, Bldg. JPP, Room 0911Z +\par Iowa City, Iowa 52242 +\par +\par }{\b\fs16\ul\cf1 +\par MEMBERS +\par +\par }{\fs16\cf1 Amendola, Marco A., M.D. 6/30/00 +\par Professor of Radiology +\par Department of Radiology (R\_109) +\par University of Miami School of Medicine +\par 1611 N.W. 12th Avenue +\par Bldg. West Wing, Room 279 +\par Miami, Florida 33136 +\par +\par Links, Jonathan M., Ph.D. 6/30/00 +\par Professor of Environmental +\par Health Sciences +\par The Johns Hopkins University +\par School of Hygiene and Public Health +\par 615 North Wolfe Street, Room 2001 +\par Baltimore, Maryland 21205\_2179 +\par +\par Ros, Pablo R., M.D., F.A.C.R. 6/30/00 +\par Vice Chairman +\par Department of Radiology +\par Brigham and Women\rquote s Hospital +\par Harvard Medical School +\par 75 Francis Street +\par Boston, Massachusetts 02115 +\par +\par Abramson, Sara J., M.D. 6/30/01 +\par Associate Attending Radiologist +\par Memorial Sloan Kettering Cancer Center +\par Department of Radiology +\par 1275 York Avenue +\par New York, New York 10021 +\par +\par Malcolm, Arnold W., M.D. 6/30/01 +\par Medical Director +\par Department of Radiation Oncology +\par St. Joseph Medical Center +\par Valley Radiotherapy Association +\par 501 S. Buena Vista +\par Burbank, California 91505 +\par +\par Hammes, Richard J., R.Ph. 6/30/01 +\par Director of Nuclear Pharmacy Services +\par Nuclear Medicine +\par Radiology Department, E3/382 +\par University of Wisconsin Hospitals & Clinics +\par 600 Highland Avenue +\par Madison, Wisconsin 53792\_3252 +\par +\par }{\b\fs16\ul\cf1 +\par +\par +\par +\par +\par +\par EXECUTIVE SECRETARY +\par +\par }{\fs16\cf1 Thomas H. Perez +\par Advisors and Consultants Staff (HFD\_21) +\par Center for Drug Evaluation and Research +\par Food and Drug Administration +\par 5600 Fishers Lane +\par Rockville, Maryland 20857 +\par (301) 827\_6758 Fax: (301) 827\_6801 +\par +\par +\par }{\f2\fs20\cf1 +\par }{\fs16\cf1 +\par }{\fs16\cf1 Tulchinsky, Mark, M.D. 6/30/02 +\par Assistant Professor of Radiology & Medicine +\par Department of Radiology +\par Penn State University Hospital +\par The Milton S. Hershey Medical Center +\par P.O. Box 850, 500 University Drive +\par Hershey, Pennsylvania 17033 +\par +\par }{\b\fs16\ul\cf1 +\par }{\f2\fs20\cf1 \page Amendola, Marco A., M.D. +\par Links, Jonathan M., Ph.D. +\par Ros, Pablo R., M.D., F.A.C.R. +\par Abramson, Sara J., M.D. +\par Ponto, Laura L. Boles, Ph.D. +\par Malcolm, Arnold W., M.D. +\par Hammes, Richard J., R.Ph. +\par Tulchinsky, Mark, M.D. +\par +\par }{\b\f2\fs20\cf1 September 14, 1999 +\par +\par }} +\ No newline at end of file diff --git a/src/plugins/testdata/rtf_fta.rtf b/src/plugins/testdata/rtf_fta.rtf @@ -0,0 +1,45 @@ +{\rtf1\ansi\ansicpg1252\uc1\deff0\stshfdbch0\stshfloch0\stshfhich0\stshfbi0\deflang1033\deflangfe1033{\fonttbl{\f0\froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;} +{\f43\fswiss\fcharset0\fprq2{\*\panose 020b0506020202030204}Arial Narrow;}{\f90\froman\fcharset238\fprq2 Times New Roman CE;}{\f91\froman\fcharset204\fprq2 Times New Roman Cyr;}{\f93\froman\fcharset161\fprq2 Times New Roman Greek;} +{\f94\froman\fcharset162\fprq2 Times New Roman Tur;}{\f95\froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f96\froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f97\froman\fcharset186\fprq2 Times New Roman Baltic;} +{\f98\froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f100\fswiss\fcharset238\fprq2 Arial CE;}{\f101\fswiss\fcharset204\fprq2 Arial Cyr;}{\f103\fswiss\fcharset161\fprq2 Arial Greek;}{\f104\fswiss\fcharset162\fprq2 Arial Tur;} +{\f105\fswiss\fcharset177\fprq2 Arial (Hebrew);}{\f106\fswiss\fcharset178\fprq2 Arial (Arabic);}{\f107\fswiss\fcharset186\fprq2 Arial Baltic;}{\f108\fswiss\fcharset163\fprq2 Arial (Vietnamese);}{\f520\fswiss\fcharset238\fprq2 Arial Narrow CE;} +{\f521\fswiss\fcharset204\fprq2 Arial Narrow Cyr;}{\f523\fswiss\fcharset161\fprq2 Arial Narrow Greek;}{\f524\fswiss\fcharset162\fprq2 Arial Narrow Tur;}{\f527\fswiss\fcharset186\fprq2 Arial Narrow Baltic;}}{\colortbl;\red0\green0\blue0; +\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128; +\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}{\stylesheet{\ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext0 Normal;} +{\*\cs10 \additive \ssemihidden Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tscellwidthfts0\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv +\ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \fs20\lang1024\langfe1024\cgrid\langnp1024\langfenp1024 \snext11 \ssemihidden Normal Table;}{\*\cs15 \additive \ul\cf2 \sbasedon10 \styrsid12654722 Hyperlink;}} +{\*\rsidtbl \rsid4340270\rsid12654722\rsid14689848\rsid14762624}{\*\generator Microsoft Word 10.0.6764;}{\info{\title OCR Document}{\author Readiris}{\operator FTA}{\creatim\yr2005\mo9\dy30\hr12\min48}{\revtim\yr2005\mo9\dy30\hr13\min18}{\version3} +{\edmins3}{\nofpages1}{\nofwords285}{\nofchars1625}{\*\company I.R.I.S.}{\nofcharsws1907}{\vern16391}}{\*\userprops {\propname Editor}\proptype30{\staticval Readiris}}\paperw12242\paperh15842\margl720\margt720 +\widowctrl\ftnbj\aenddoc\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\hyphcaps0\horzdoc\dghspace120\dgvspace120\dghorigin1701\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale75\nolnhtadjtbl\rsidroot14762624 \fet0\sectd +\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4 +\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (} +{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ql \li0\ri0\nowidctlpar\faauto\rin0\lin0\itap0 +\fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\f1\fs10\insrsid14762624 +\par }\pard \ql \li0\ri0\sl-278\slmult0\nowidctlpar\phmrg\posx561\posy883\absw1838\faauto\rin0\lin0\itap0 {\insrsid14762624 U.S. Department Of Transportation }{\b\insrsid14762624 Federal Transit Administration +\par }\pard \qj \li0\ri0\sl-297\slmult0\nowidctlpar\phmrg\posx4166\posy883\absw1368\faauto\rin0\lin0\itap0 {\insrsid14762624 Headquarters +\par }\pard \ql \li0\ri0\sl-268\slmult0\nowidctlpar\phmrg\posx7032\posy917\absw2068\faauto\rin0\lin0\itap0 {\fs20\insrsid14762624 400 Seventh St. S.W. Washington, D.C. 20590 +\par }\pard \qj \li0\ri0\sl-297\slmult0\nowidctlpar\phmrg\posx7704\posy2592\absw1512\faauto\rin0\lin0\itap0 {\f43\fs26\insrsid14762624 SEP 1 5 2005 +\par }\pard \qj \li0\ri0\sl-297\slmult0\nowidctlpar\phmrg\posx566\posy3638\absw1660\faauto\rin0\lin0\itap0 {\insrsid14762624 Dear Colleague: +\par }\pard \ql \li0\ri0\sl-273\slmult0\nowidctlpar\phmrg\posx561\posy4190\absw8764\faauto\rin0\lin0\itap0 {\insrsid14762624 +I would like to make you aware and encourage your attendance at the International Right of Way Association's (IRW A) Uniform Act Symposium in Anaheim, California, November 7-9,2005. This rare yet complete training opportunity consists of over 70 sessions +r +elating to real estate and the laws that govern its use. I encourage your agency to send key employees involved in real estate activities. A Symposium of this magnitude, covering the full range of right of way activities, is an outstanding opportunity for + right of way professionals. +\par }\pard \ql \li0\ri0\sl-273\slmult0\nowidctlpar\phmrg\posx571\posy6394\absw8673\faauto\rin0\lin0\itap0 {\insrsid14762624 +The Symposium is a comprehensive three-day educational event specifically aimed at the 2005 Uniform Act regulatory revisions and other areas of interest to the Federal Transit Administration (FT A). As a result of the FT A/IR W +A Partnering Agreement signed in Dallas, Texas, July 14,2005, IRW A has added sessions of specific interest to the FTA family, such as Transit Oriented Development, Risk Assessment in ROW, Public/Private Partnerships, ROW Cost Estimating, Integrating ROW +into the National Environmental Protection Act Process, and Developing a ROW Plan. +\par }\pard \ql \li0\ri0\sl-273\slmult0\nowidctlpar\phmrg\posx576\posy8611\absw8414\faauto\rin0\lin0\itap0 {\insrsid14762624 +Grantees will learn about the attributes that contribute to a successful transit real estate acquisition program. This will be especially helpful to all grantees that acquire real property and especially helpful for grantees working on New Start projects. + +\par }\pard \ql \li0\ri0\sl-268\slmult0\nowidctlpar\phmrg\posx580\posy9710\absw8563\faauto\rin0\lin0\itap0 {\insrsid14762624 For more information on registration, motels, agenda and session summaries go to the IRW A website at }{\field{\*\fldinst { +\ul\insrsid12654722 HYPERLINK "http://irwaonline.org/" }{\ul\insrsid12654722\charrsid5651136 {\*\datafield +00d0c9ea79f9bace118c8200aa004ba90b02000000170000001700000068007400740070003a002f002f0069007200770061006f006e006c0069006e0065002e006f00720067002f000000e0c9ea79f9bace118c8200aa004ba90b2e00000068007400740070003a002f002f0069007200770061006f006e006c0069006e00 +65002e006f00720067002f000000}}}{\fldrslt {\cs15\ul\cf2\insrsid12654722\charrsid5651136 http://irwaonline.org/}}}{\ul\insrsid14762624 .}{\ul\insrsid12654722 }{\insrsid12654722\charrsid12654722 }{\insrsid14762624\charrsid12654722 lf}{ +\insrsid12654722\charrsid12654722 }{\insrsid14762624\charrsid12654722 you}{\insrsid12654722\charrsid12654722 }{\insrsid14762624\charrsid12654722 have}{\insrsid12654722\charrsid12654722 }{\insrsid14762624\charrsid12654722 questions }{\insrsid14762624 +on allocations of training funds for grantees please contact Ron Fannin, TPM-20 at (202)-366-1647 }{\ul\insrsid14762624 (ronald.fannin@fta.dot. gov). +\par }\pard \ql \fi168\li0\ri0\sl-633\slmult0\nowidctlpar\phmrg\posx4752\posy11376\absw4185\faauto\rin0\lin0\itap0 {\insrsid14762624 Sincerely, }{\fs46\insrsid14762624 ~~~ +\par }\pard \ql \li0\ri0\sl-273\slmult0\nowidctlpar\phmrg\posx4910\posy12466\absw2774\faauto\rin0\lin0\itap0 {\insrsid14762624 Susan E. Schruth Associate Administrator for +\par }\pard \ql \li120\ri0\sl-273\slmult0\nowidctlpar\phmrg\posx4910\posy12466\absw2774\faauto\rin0\lin120\itap0 {\insrsid14762624 Program Management +\par }} +\ No newline at end of file diff --git a/src/plugins/testdata/rtf_nasa.rtf b/src/plugins/testdata/rtf_nasa.rtf @@ -0,0 +1,28 @@ +{\rtf1\ansi\ansicpg1252\uc1 \deff0\deflang1033\deflangfe1033{\fonttbl{\f0\froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f2\fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New;} +{\f33\froman\fcharset238\fprq2 Times New Roman CE;}{\f34\froman\fcharset204\fprq2 Times New Roman Cyr;}{\f36\froman\fcharset161\fprq2 Times New Roman Greek;}{\f37\froman\fcharset162\fprq2 Times New Roman Tur;} +{\f38\froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f39\froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f40\froman\fcharset186\fprq2 Times New Roman Baltic;}{\f49\fmodern\fcharset238\fprq1 Courier New CE;} +{\f50\fmodern\fcharset204\fprq1 Courier New Cyr;}{\f52\fmodern\fcharset161\fprq1 Courier New Greek;}{\f53\fmodern\fcharset162\fprq1 Courier New Tur;}{\f54\fmodern\fcharset177\fprq1 Courier New (Hebrew);} +{\f55\fmodern\fcharset178\fprq1 Courier New (Arabic);}{\f56\fmodern\fcharset186\fprq1 Courier New Baltic;}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0; +\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;}{\stylesheet{ +\ql \li0\ri0\nowidctlpar\faauto\adjustright\rin0\lin0\itap0 \fs20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext0 Normal;}{\*\cs10 \additive Default Paragraph Font;}}{\info{\author WLBEHNKE}{\operator INTELLISOURCE} +{\creatim\yr2000\mo2\dy11\hr7\min11}{\revtim\yr2001\mo5\dy30\hr7\min26}{\version6}{\edmins12}{\nofpages1}{\nofwords158}{\nofchars905}{\*\company GSFC/NASA}{\nofcharsws0}{\vern8269}}\margl1440\margr1440 +\widowctrl\ftnbj\aenddoc\ftnrestart\aftnnar\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\hyphcaps0\horzdoc\dghspace120\dgvspace120\dghorigin1701\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind4\viewscale100\nolnhtadjtbl \fet0\sectd +\sbknone\linex0\endnhere\sectdefaultcl {\*\pnseclvl1\pndec\pnstart1 {\pntxta .}}{\*\pnseclvl2\pnlcltr\pnstart1 {\pntxta .}}{\*\pnseclvl3\pnlcrm\pnstart1 {\pntxta .}}{\*\pnseclvl4\pndec\pnstart1 {\pntxtb (}{\pntxta )}}{\*\pnseclvl5\pnlcltr\pnstart1 +{\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcrm\pnstart1 {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pndec\pnstart1 {\pntxta .}}{\*\pnseclvl8\pnlcltr\pnstart1 {\pntxta .}}{\*\pnseclvl9\pnlcrm\pnstart1 }\pard\plain +\ql \li0\ri0\nowidctlpar\faauto\adjustright\rin0\lin0\itap0 \fs20\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\f2\fs24 HISTORICALLY BLACK COLLEGE OR UNIVERSITY AND MINORITY INSTITUTION REPRESENTATION (52.226-2) (MAY 2001) +\par +\par (a) Definitions. As used in this provision-- +\par \'93Historically black college or university\'94 + means an institution determined by the Secretary of Education to meet the requirements of 34 CFR 608.2. For the Department of Defense, the National Aeronautics and Space Administration, and the Coast Guard, the term also includes any nonprofit research +institution that was an integral part of such a college or university before November 14, 1986. +\par \'93Minority institution\'94 means an institution of higher education meeting the requirements of Section 1046(3) of the Higher Education Act of 1965 (20 U.S.C. 1067k, + including a Hispanic-serving institution of higher education as defined in Section 316(b)(1) of the Act (20 U.S.C. 1101a)). +\par +\par (b) Representation. The offeror represents that it-- +\par +\par [ ] is.[ ].is not a historically black college or university; +\par [ ].is [..].is not a minority institution. +\par +\par }\pard \qc \li0\ri0\nowidctlpar\faauto\adjustright\rin0\lin0\itap0 {\f2\fs24 (End of provision) +\par }\pard \ql \li0\ri0\nowidctlpar\faauto\adjustright\rin0\lin0\itap0 {\f2\fs24 +\par }} +\ No newline at end of file diff --git a/src/plugins/testdata/rtf_nih.rtf b/src/plugins/testdata/rtf_nih.rtf @@ -0,0 +1,38 @@ +{\rtf1\ansi\ansicpg1252\uc1\deff0\stshfdbch0\stshfloch0\stshfhich0\stshfbi0\deflang1033\deflangfe1033{\fonttbl{\f0\froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f190\froman\fcharset238\fprq2 Times New Roman CE;} +{\f191\froman\fcharset204\fprq2 Times New Roman Cyr;}{\f193\froman\fcharset161\fprq2 Times New Roman Greek;}{\f194\froman\fcharset162\fprq2 Times New Roman Tur;}{\f195\froman\fcharset177\fprq2 Times New Roman (Hebrew);} +{\f196\froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f197\froman\fcharset186\fprq2 Times New Roman Baltic;}{\f198\froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255; +\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0; +\red128\green128\blue128;\red192\green192\blue192;}{\stylesheet{\ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \b\fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 \snext0 Normal;}{\*\cs10 \additive \ssemihidden +Default Paragraph Font;}{\*\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\tscellwidthfts0\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv +\ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \fs20\lang1024\langfe1024\cgrid\langnp1024\langfenp1024 \snext11 \ssemihidden Normal Table;}}{\*\latentstyles\lsdstimax156\lsdlockeddef0}{\*\rsidtbl \rsid2579519\rsid3085999 +\rsid8813573\rsid11105908\rsid13661264\rsid15167634\rsid16073026}{\*\generator Microsoft Word 11.0.6568;}{\info{\author NINDS}{\operator NINDS}{\creatim\yr2006\mo9\dy29\hr12\min2}{\revtim\yr2006\mo9\dy29\hr12\min30}{\version3}{\edmins28}{\nofpages1} +{\nofwords170}{\nofchars973}{\*\company NIH}{\nofcharsws1141}{\vern24579}}\widowctrl\ftnbj\aenddoc\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace241\dgvspace180\dghorigin1800\dgvorigin1440\dghshow1\dgvshow2 +\jexpand\viewkind1\viewscale100\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nojkernpunct\rsidroot13661264 \fet0\sectd +\psz1\linex0\endnhere\sectlinegrid360\sectdefaultcl\sectrsid3085999\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang +{\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7 +\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain +\ql \li0\ri0\widctlpar\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \b\fs24\lang1033\langfe1033\cgrid\langnp1033\langfenp1033 {\insrsid8813573 September 29, 2006 +\par +\par Statement of Work}{\insrsid16073026 +\par }{\insrsid8813573 Dvir Blivis +\par +\par +\par Dr. Dvir Blivis is coming to NIH }{\insrsid2579519 (10/18/06 0 11/18/06) }{\insrsid8813573 to perform collaborative work on a project concerned with the role of funicular neurons in locomotor-like activity in the neonatal mouse. Specifically, he +will cut and stain sections of the spinal cord that have been labeled with fluorescent dyes. He will analyze these sections on a confocal microscope and record and tabulate his findings. He will also perform morphological analysis on the anatomical materi +al with special consideration of the synaptic connections of the labeled funicular neurons with motoneurons. +\par +\par Dr. Michael O\rquote Donovan, Chief Investigator, DNS, DIR, NINDS would like to provide Dr. Blivis with lodging and meals while at NIH, and reimburse his airfare. +\par +\par Airfare }{\insrsid2579519 \tab }{\insrsid8813573 $1,973.14 +\par }{\insrsid2579519 Lodging\tab $1,449.00 +\par Meals \tab $1,050.00 +\par }{\insrsid11105908 +\par }{\insrsid2579519 TOTAL\tab $4,472.14 +\par +\par }{\insrsid15167634 Request that a check be issued for meals and lodging in the amount of $2499 by no later than October 20, 2006. +\par +\par Reimbursement for airfare will be requested upon submission of airline ticket stub.}{\insrsid2579519 +\par }{\insrsid8813573 +\par }{\insrsid15167634 The period of performance shall be October 18, 2006 through November 30, 2006.}{\insrsid8813573 +\par +\par }} +\ No newline at end of file diff --git a/src/plugins/testdata/rtf_review.rtf b/src/plugins/testdata/rtf_review.rtf @@ -0,0 +1,35 @@ +{\rtf1\ansi\ansicpg1252\uc1\deff0 +{\fonttbl{\f0\froman\fcharset0 Times New Roman;}} +{\*\generator libextractor test fixture 1.0;} +{\*\revtbl {Unknown;}{Records Management Office;}{Office of the Inspector General;}} +{\info +{\title Quarterly Compliance Review \u8212 ? 2019} +{\subject Records management} +{\author Records Management Office} +{\operator Office of the Inspector General} +{\*\company Example Agency \'96 Region 5} +{\manager Division Chief} +{\category Internal} +{\keywords compliance, records, retention} +{\doccomm Draft circulated for comment} +{\hlinkbase http://www.example.org/records/} +{\creatim\yr2019\mo3\dy4\hr9\min15} +{\revtim\yr2019\mo3\dy11\hr16\min48} +{\printim\yr2019\mo3\dy12\hr8\min5} +{\version4}{\edmins37}{\nofpages2}{\nofwords480}{\nofchars2650}{\nofcharsws3110}{\vern16385}} +{\*\userprops +{\propname _AuthorEmail}\proptype30{\staticval records@example.org} +{\propname _AuthorEmailDisplayName}\proptype30{\staticval Records Management Office} +{\propname _EmailSubject}\proptype30{\staticval Please review by Friday} +{\propname Retention Schedule}\proptype30{\staticval GRS 4.1}} +{\*\template C:\\Program Files\\Example Agency\\Templates\\Memorandum.dot} +{\*\password 00000000} +\paperw12240\paperh15840\margl1440\margr1440 +\pard\plain \f0\fs24 +The retention schedule for quarterly reports is under review. +{\revised\revauth1\revdttm1198730144 The office proposes a five year retention period.} +{\deleted\revauthdel2\revdttmdel-1485618617 The previous wording said seven years.} +\crauth1\crdate661871664 Formatting corrected. +{\*\atnid RMO}{\*\atnauthor Records Management Office}\chatn +{\*\annotation {\*\atnref 1}{\*\atndate 661871664}\pard\plain \f0\fs20 Please cite the general records schedule here.} +\par}