. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. # # # They are: a bare `import', a second import whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for 131 measurements. Every other case # # below reads the filesystem or dpkg with shell builtins, # # or greps the probe's output. # # # There is NO fourth for a shipped executable: setup.py # # passes neither `scripts' nor `entry_points', so nothing # # lands in /usr/bin and there is nothing to reach through a # # shebang. `no-bin-script' asserts that absence rather than # # leaving it unstated. olefile DOES carry a `main()' and is # # runnable as `python3 -m olefile.olefile', which this suite # # deliberately does not spend a start on -- it prints a # # human report and asserts nothing the probe does not. # # # The work inside the probe start is six containers of # # 5632 bytes each, written to and read back from a # # directory -- microseconds beside an interpreter start on # # this arm, which is tens of seconds before the first line # # of the program runs. Nothing here is quadratic in # # anything: the largest payload is 4096 bytes. # # # 1200 is 400s per start, matching 99-python3-pyparsing's # # budget for its own three and roughly six times the ~60s a # # python3 start has measured at on this arm. # # # A suite-timeout red is a batch-wide cost, so if the three # # starts prove slower than that on target the RIGHT fix is # # this number, not fewer cases. # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed replaces # # this note in the test plan rather than in this comment, # # which would cost an image build to keep the shipped suite # # byte-identical. # # python3-olefile 0.47 (meta-openembedded/meta-python # recipes-devtools/python/python3-olefile_0.47.bb -- `inherit pypi setuptools3', # PyPI sdist, `PYPI_PACKAGE_EXT = "zip"') on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES. # ============================================================================ # # A READER AND IN-PLACE WRITER FOR OLE2 CONTAINERS -- Microsoft's Compound File # Binary Format, the wrapper around a pre-2007 Word/Excel/PowerPoint document, # an Outlook .msg, a Thumbs.db. The container is a filesystem in a file: 512-byte # sectors, a FAT that chains them, a directory tree of storages and streams, and # a second smaller allocator (the MiniFAT) for streams below a 4096-byte cutoff. # # olefile parses that structure and hands back streams as `io.BytesIO'. It does # NOT create containers and it does not resize streams: `write_stream' overwrites # an existing stream with content of EXACTLY the same length, in place. So the # fixture this suite measures against cannot be produced by the package itself, # and is built byte by byte by the probe -- see below. # # ============================================================================ # WHY IT PORTS, AND WHAT IT WAITS ON. # ============================================================================ # # PURE PYTHON, AND THE DEPENDENCY SET IS COMPLETE AS WRITTEN. This image's # python3 is ONE STATIC BINARY with an empty lib-dynload and no dynamic loader # (BRINTOS-1476), so a module carrying a compiled extension installs cleanly and # cannot be imported. olefile declares no ext_modules and ships no `.so' # (`pkg-no-so' asserts that off the shipped file list rather than off the # source), so this port carries no red attributable to BRINTOS-2046. # # Its whole module-scope import surface is `io, sys, struct, array, os.path, # datetime, logging, warnings, traceback' (olefile/olefile.py lines 104-106). # CHECKED against poky 3.12.13's own python3-manifest.json rather than assumed: # io.py, struct.py, os.py, posixpath.py, warnings.py, traceback.py and # lib-dynload/array are all `core' entries; datetime.py is the `datetime' split # and the logging package is the `logging' split. meta-python's recipe declares # exactly `python3-core python3-datetime python3-logging', so the set is right # as written and this port needs NO bbappend. `pkg-depends-*' asserts each of # the three by name, so a poky reshuffle that moved one says which. # # `array' is worth one sentence because it is the one lib-dynload entry in that # list. On this arm configure takes its `wasm32|wasm64) MODULE_BUILDTYPE=static' # case, so every stdlib extension is linked into the interpreter and lib-dynload # is empty by construction -- `array' is a builtin here, not a .so, and the bare # `import' below is what proves it. # # ============================================================================ # THE FIXTURE IS BUILT BY THE PROBE, NOT SHIPPED. # ============================================================================ # # data/python3-olefile-probe.py writes its own CFBF v3 container: a 512-byte # header with the documented signature and sector shift, ONE FAT sector, ONE # directory sector carrying a root storage and one stream entry, and the # stream's eight data sectors. 5632 bytes in total. Every field the assertions # read is a literal in that file. # # THE STREAM IS EXACTLY 4096 BYTES, which is the header's mini-stream cutoff. # olefile routes to the MiniFAT on `size < minisectorcutoff', so at exactly the # cutoff the stream is a NORMAL one -- the fixture therefore needs no MiniFAT # sector and no mini stream under the root, which is what keeps the builder # short enough to read. `stream-not-mini' pins that routing. # # Nothing is downloaded and no Microsoft document is needed, so the suite reaches # no device, no peer and no terminal. # # ============================================================================ # THE FOUR FINDINGS, ALL UPSTREAM'S AND NONE OF THEM THIS ARM'S. # ============================================================================ # # Each was MEASURED on an x86-64 python3.12 control against the same 0.47 sources # the SRC_URI sha256 pins, before it was written down here, so a reader can tell # a property of the package from a property of the platform. None is filed: # they are upstream 0.47's behaviour on every platform, and this port neither # introduces nor can fix them. See docs/conventions/no-public-upstream-reports.md. # # 1. A FAT ENTRY POINTING AT ITSELF IS NOT DETECTED AT ALL. This port's ticket # predicted that it would "raise rather than loop". It does neither: olefile # bounds the walk by `nb_sectors = ceil(size/sectorsize)' computed from the # DECLARED stream size, so a self-loop is not an infinite loop -- and no # length check ever fires, because the bounded walk returns exactly the # declared 4096 bytes. `loop-is-sector-repeated' measures what those bytes # actually are: the stream's FIRST data sector, repeated eight times. No # exception at ANY defect level, and `parsing_issues' is EMPTY. A caller gets # silently wrong data. # # 2. A DEFECT IS NOT AN EXCEPTION BY DEFAULT. `OleFileIO' defaults to # `raise_defects=DEFECT_FATAL', under which a DEFECT_INCORRECT is APPENDED TO # `parsing_issues' and not raised. So the out-of-range start sector returns # zero bytes and two recorded issues rather than an error, and the truncated # container returns 512 of 4096 bytes and three. Only # `raise_defects=DEFECT_INCORRECT' -- the level upstream's own docstring # recommends for "a security-oriented application" -- raises. Both levels are # measured for all three malformations, because the difference IS the finding. # # 3. THE ERROR CLASSES ARE NOT REACHABLE FROM THE PACKAGE NAMESPACE. # `olefile/__init__.py' is `from .olefile import *' plus four dunders, and the # submodule's `__all__' names `OleFileIONotClosed' alone out of the error # tree. So `olefile.OleFileError' and `olefile.NotOleFileError' are # AttributeErrors and a caller must reach through `olefile.olefile' -- or # catch IOError, which both subclass. `filetime2datetime' is unreachable the # same way. Recorded, not filed: it is upstream's own export list. # # 4. THE ROOT ENTRY IS NOT ADDRESSABLE BY NAME. `exists("Root Entry")' is False # and `getctime("Root Entry")' raises, because `_find' walks the root's KIDS. # Its timestamps are reachable only off `ole.root'. A caller iterating # `listdir()' never sees it either -- `listdir' returns the one stream. # # The `write_stream' round trip is NOT a finding: it works exactly as documented, # and the suite asserts it byte-for-byte from both sides -- through olefile, and # by comparing the container on disk against the original outside the eight # rewritten sectors, which is the half a re-read through the same parser cannot # vouch for. P=python3-olefile # ============================================================================ # The suite proper. # ============================================================================ # # `suite-canary' is emitted FIRST and unconditionally. A suite that dies before # its first result line is indistinguishable from one that was never scheduled, # so this one line is what separates "ran and found nothing" from "did not run". bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKG=$SITE/olefile DI=$SITE/olefile-0.47.dist-info PKGLIST=/var/lib/dpkg/info/python3-olefile.list PROBE=$BST_DATA/python3-olefile-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-olefile.$$ mkdir -p "$W" "$W/fix" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ---------------------------------------------------------------- packaging -- # WHERE THE PACKAGING SAYS THE MODULE LANDS. These run before any import, so a # red here says the .deb is wrong rather than that the module misbehaves. check $P/site-packages-dir test -d "$SITE" check $P/package-dir test -d "$PKG" check $P/package-init test -s "$PKG/__init__.py" # It is a PACKAGE of two modules, not a single flat one. A release that # flattened it would still import and still pass every behavioural case, so the # shape is asserted directly. `olefile/olefile.py' is where all 2696 lines live; # `__init__.py' is the re-export. check $P/no-flat-module test ! -f "$SITE/olefile.py" check $P/module-olefile test -s "$PKG/olefile.py" # NO EXECUTABLE. setup.py passes neither `scripts' nor `entry_points', so # nothing lands in /usr/bin. Asserted so that a future release which started # shipping one is noticed here. check $P/no-bin-script test ! -e /usr/bin/olefile # --------------------------------------------------------------- dist-info --- # The version is read from METADATA rather than from the module, so a mismatch # between what the packaging claims and what imports is visible as a mismatch. # `dist-version' below reads the module's own `__version__' for the other half. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.47" cat "$DI/METADATA" check_out $P/dist-info-name "Name: olefile" cat "$DI/METADATA" check $P/dist-info-wheel test -f "$DI/WHEEL" check $P/dist-info-record test -f "$DI/RECORD" check_out $P/dist-info-toplevel "olefile" cat "$DI/top_level.txt" # PURE PYTHON, stated by the packaging itself. `Root-Is-Purelib: true' is the # wheel's own claim that nothing in it is architecture-specific, which is the # property that makes this package portable to this arm at all; `Tag: # py3-none-any' is the same claim in the filename's coordinates. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag "Tag: py3-none-any" cat "$DI/WHEEL" # ------------------------------------------------------------ the .deb file --- # The installed file list, read once into $W. The copy is what the loop below # redirects from: a stdin redirect the top-level shell performs on the virtiofs # root makes dup2(2) return 513 and ENDS the suite silently (brintos/linux#73), # and $W is on tmpfs. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # NO COMPILED EXTENSION, asserted off the shipped file list rather than off the # source. This image's python3 has an empty lib-dynload and no dynamic loader # (BRINTOS-1476), so a single .so here would mean a package that installs # cleanly and cannot import -- the BRINTOS-2046 shape. check_eq $P/pkg-no-so "" \ "$(grep -c '\.so$' "$W/pkgf" 2>/dev/null | grep -v '^0$')" # Every payload path the package ships is under site-packages, and is a .py, a # .pyc, or dist-info metadata. Anything else is a file this port did not expect # to be shipping and wants looking at. _bad="" while IFS= read -r _f; do case "$_f" in ''|/usr/lib/python3.12/site-packages) continue ;; */) continue ;; esac [ -d "$_f" ] && continue case "$_f" in *.py|*.pyc) ;; # dist-info metadata. Which of these the build backend writes is a property # of SETUPTOOLS rather than of this package, so the whole standard set is # allowed: pinning the exact list would red on a setuptools change that says # nothing about olefile. `pkg-no-so' above carries the real claim. */METADATA|*/WHEEL|*/RECORD|*/INSTALLER|*/LICENSE*|*/entry_points.txt|*/top_level.txt) ;; *) _bad="$_bad $_f" ;; esac done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ------------------------------------------------------------- dpkg status --- sed -n '/^Package: python3-olefile$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" check_out $P/pkg-version "Version: 0.47-r0" cat "$W/st" check_out $P/pkg-arch "Architecture: wasm32" cat "$W/st" # THE DEPENDENCY SET IS THE PORT'S CLAIM THAT IT WAITS ON NOTHING. Every name # here is a sub-package of the ported python3 (BRINTOS-1476), so each is # asserted individually -- a bare "the Depends line exists" would pass on a # stanza that had lost one of them, and the loss would surface as an ImportError # at the first use that reached that stdlib module. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" for _d in python3-core python3-datetime python3-logging; do check_out $P/pkg-depends-$_d "$_d" cat "$W/dep" done # ------------------------------------------------------------------ import --- # THE FIRST OF THREE PYTHON STARTS. If the Depends set above were short by one # sub-package -- or if `array' were a .so this interpreter cannot load rather # than the builtin it is here -- this is the case that would say so. check $P/import python3 -c 'import olefile' # THE SECOND. A package that imports but WARNS on the way in is a different # defect from one that fails, and it is invisible to the case above because the # exit status is 0 either way. The assertion is that stderr is EMPTY. check_eq $P/import-quiet "" \ "$(python3 -c 'import olefile' 2>&1 >/dev/null)" # ------------------------------------------------------------------- probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "probe missing at $PROBE" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # THE THIRD START, and the one that carries the suite. Bounded at 900s inside # the 1200s suite budget so that a hung probe is reported as a probe overrun # with the last line it printed, rather than as a suite timeout that says # nothing about where it stopped. bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" "$W/fix" OUT="$W/probe" # pv : the value the probe emitted for , or empty. `head -n 1' # because a key is emitted once and a duplicate would otherwise concatenate; # `cut -f2-' because several values legitimately contain `='. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # The probe reached its end. Every case below reads a value the probe printed, # so a probe that stopped early would otherwise turn into a run of empty-string # mismatches that all name the wrong thing. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "131" "$(pv emitted_count)" # ---------------------------------------------------------------- identity --- # WHICH COPY ANSWERED. Without this the behavioural cases below would be # statements about whatever was first on sys.path. check_eq $P/module-answered "$PKG/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/dist-version "0.47" "$(pv version)" check_eq $P/module-date "2023-12-01" "$(pv date)" check_eq $P/author-present "yes" "$(pv author_present)" # `__all__' IS THE PUBLIC SURFACE, and it is asserted whole rather than sampled: # finding 3 above is precisely that two names a caller would expect are missing # from it, and a list checked name by name is the only form in which that is # visible. Sorted, so the assertion does not depend on upstream's line order. check_eq $P/all-names \ "DEFAULT_PATH_ENCODING,DEFECT_FATAL,DEFECT_INCORRECT,DEFECT_POTENTIAL,DEFECT_UNSURE,DIFSECT,ENDOFCHAIN,FATSECT,FREESECT,MAGIC,MAXREGSECT,MAXREGSID,MINIMAL_OLEFILE_SIZE,NOSTREAM,OleFileIO,OleFileIONotClosed,OleMetadata,STGTY_EMPTY,STGTY_LOCKBYTES,STGTY_PROPERTY,STGTY_ROOT,STGTY_STORAGE,STGTY_STREAM,UNKNOWN_SIZE,WORD_CLSID,enable_logging,isOleFile" \ "$(pv all_names)" # THE FORMAT'S OWN CONSTANTS, asserted against the specification's values rather # than against whatever the module happens to define. These are what the probe's # hand-built container is encoded with, so a change in any of them would make # the fixture and the parser disagree -- and the disagreement would otherwise # surface as an unattributable parse failure further down. check_eq $P/magic "d0cf11e0a1b11ae1" "$(pv magic_hex)" check_eq $P/minimal-size "1536" "$(pv minimal_size)" check_eq $P/stgty-storage "1" "$(pv stgty_storage)" check_eq $P/stgty-stream "2" "$(pv stgty_stream)" check_eq $P/stgty-root "5" "$(pv stgty_root)" check_eq $P/endofchain "fffffffe" "$(pv endofchain)" check_eq $P/freesect "ffffffff" "$(pv freesect)" check_eq $P/nostream "ffffffff" "$(pv nostream)" check_eq $P/maxregsid "fffffffa" "$(pv maxregsid)" # THE FOUR DEFECT LEVELS are the subject of finding 2, so their values are # pinned: `DEFECT_INCORRECT' (30) is strictly below the DEFAULT `DEFECT_FATAL' # (40), which is exactly why a malformed FAT is recorded rather than raised. check_eq $P/defect-unsure "10" "$(pv defect_unsure)" check_eq $P/defect-potential "20" "$(pv defect_potential)" check_eq $P/defect-incorrect "30" "$(pv defect_incorrect)" check_eq $P/defect-fatal "40" "$(pv defect_fatal)" # ------------------------------------------------------------- the fixture --- # THE CONTAINER THE PROBE BUILT, before anything parses it. A red here is a # broken fixture rather than a broken package, and separating the two is the # whole reason these two cases come first. check_eq $P/fixture-size "5632" "$(pv good_size)" check_eq $P/fixture-sectors "10" "$(pv good_sectors)" # ------------------------------------------------------------- isOleFile ---- # THE SNIFF, over the three input shapes the function accepts and the three # negative controls the ticket names. The positive is the hand-built container; # the negatives are an EMPTY file, a file SHORTER than the 8-byte signature, and # a full-size container whose signature's last byte is wrong. check_eq $P/is-ole-good "True" "$(pv is_ole_good)" check_eq $P/is-ole-empty "False" "$(pv is_ole_empty)" check_eq $P/is-ole-short "False" "$(pv is_ole_short)" check_eq $P/is-ole-badsig "False" "$(pv is_ole_badsig)" # THE `data=' ARM APPLIES A SIZE FLOOR THE PATH ARM DOES NOT. With `data=', # anything below MINIMAL_OLEFILE_SIZE (1536) is False before the signature is # even looked at; with a path, only the first 8 bytes are read and no size check # happens at all. So the SAME 512-byte prefix of a valid container is False by # `data=' -- which is what `is-ole-data-tooshort' pins. A caller who moves from # one arm to the other and expects the same answer is the person this case is # written for. check_eq $P/is-ole-data-good "True" "$(pv is_ole_data_good)" check_eq $P/is-ole-data-badsig "False" "$(pv is_ole_data_badsig)" check_eq $P/is-ole-data-tooshort "False" "$(pv is_ole_data_tooshort)" check_eq $P/is-ole-fileobj "True" "$(pv is_ole_fileobj)" check_eq $P/is-ole-fileobj-empty "False" "$(pv is_ole_fileobj_empty)" # A FILE-LIKE OBJECT IS REWOUND by the sniff, which is what makes the documented # "sniff, then parse the same handle" sequence work at all. check_eq $P/is-ole-rewinds "0" "$(pv is_ole_rewinds)" # A MISSING PATH is the ordinary OSError, not a False and not an olefile error: # the function opens the file itself. check_eq $P/is-ole-missing-exc "FileNotFoundError" "$(pv is_ole_missing_exc)" # ----------------------------------------------------------------- the tree -- # LISTDIR AGAINST THE EXACT TREE THE PROBE BUILT: one stream under the root, and # no storages. Path components are joined with `/' for transport. check_eq $P/open-issues "0" "$(pv open_issues)" check_eq $P/listdir "BrintosStream" "$(pv listdir)" check_eq $P/listdir-storages "" "$(pv listdir_storages)" check_eq $P/exists-stream "True" "$(pv exists_stream)" check_eq $P/exists-absent "False" "$(pv exists_absent)" # FINDING 4. The root storage is NOT addressable by name -- `_find' walks the # root's kids, so the root itself is not among them. check_eq $P/exists-root "False" "$(pv exists_root)" check_eq $P/root-name "Root Entry" "$(pv root_name)" check_eq $P/root-kids "BrintosStream" "$(pv root_kids)" # `get_type' answers STGTY_STREAM (2) for the stream and FALSE -- not 0, and not # an exception -- for a name that is not there. check_eq $P/get-type-stream "2" "$(pv get_type_stream)" check_eq $P/get-type-absent "False" "$(pv get_type_absent)" # THE HEADER FIELDS THE PARSER READ BACK, against the literals the probe # encoded. A container the parser mis-read would still list and still open, so # these are asserted rather than inferred from the stream coming back whole. check_eq $P/sector-size "512" "$(pv sector_size)" check_eq $P/mini-cutoff "4096" "$(pv mini_cutoff)" check_eq $P/dll-version "3" "$(pv dll_version)" check_eq $P/num-fat-sectors "1" "$(pv num_fat_sectors)" check_eq $P/first-dir-sector "1" "$(pv first_dir_sector)" # ------------------------------------------------------------- the stream ---- # THE EXACT BYTES. The payload is a function of its offset, so a container whose # sectors were followed in the wrong order could not compare equal -- which is # what makes `stream-matches' a statement about the FAT walk and not only about # the content. The two 16-byte ends are pinned as well, so a mismatch says # whether the walk went wrong at the head or the tail. check_eq $P/get-size "4096" "$(pv get_size)" check_eq $P/stream-len "4096" "$(pv stream_len)" check_eq $P/stream-matches "yes" "$(pv stream_matches)" check_eq $P/stream-head "030a11181f262d343b424950575e656c" "$(pv stream_sha_head)" check_eq $P/stream-tail "c8cfd6dde4ebf2f9050c131a21282f36" "$(pv stream_sha_tail)" check_eq $P/stream-size-attr "4096" "$(pv stream_size_attr)" check_eq $P/stream-is-bytesio "yes" "$(pv stream_is_bytesio)" # NOT A MINI STREAM. 4096 is exactly the cutoff and the routing test is `<', so # this stream lives in the normal FAT -- which is why the fixture needs no # MiniFAT sector at all. check_eq $P/stream-not-mini "yes" "$(pv stream_not_mini)" # The directory entry, read straight off `direntries[1]', against the fields the # probe encoded 128 bytes at a time. check_eq $P/entry-name "BrintosStream" "$(pv entry_name)" check_eq $P/entry-type "2" "$(pv entry_type)" check_eq $P/entry-size "4096" "$(pv entry_size)" check_eq $P/entry-start "2" "$(pv entry_start)" # THE NAMES THAT ARE NOT THERE, and the ROOT, are OSError rather than KeyError: # olefile raises through its own IOError-derived tree, so `except OSError' is # what a caller writes. Asserted by TYPE, never by message. check_eq $P/get-size-absent-exc "OSError" "$(pv get_size_absent_exc)" check_eq $P/openstream-absent-exc "OSError" "$(pv openstream_absent_exc)" check_eq $P/openstream-root-exc "OSError" "$(pv openstream_root_exc)" # A STREAM OPENED FROM A CLOSED CONTAINER is refused rather than returning an # empty one -- upstream issue #81, fixed and pinned here. check_eq $P/closed-openstream-exc "OSError" "$(pv closed_openstream_exc)" # ------------------------------------------------------------ the timestamps -- # THE DIRECTORY ENTRY'S TIMES, against the FILETIME literals the probe encoded -- # 100-nanosecond ticks since 1601-01-01, converted by olefile's own # `filetime2datetime'. Both the value and the literal it was built from are # emitted, so a red says whether the encoder or the decoder moved. check_eq $P/ctime "2023-12-01 00:00:00" "$(pv ctime)" check_eq $P/mtime "2024-06-15 12:34:56" "$(pv mtime)" check_eq $P/ctime-literal "2023-12-01 00:00:00" "$(pv ctime_literal)" check_eq $P/mtime-literal "2024-06-15 12:34:56" "$(pv mtime_literal)" # A ZERO FILETIME IS `None', NOT 1601-01-01. The root entry carries zeros in this # fixture, which is what every real container does too. check_eq $P/root-ctime-none "yes" "$(pv root_ctime_none)" check_eq $P/root-mtime-none "yes" "$(pv root_mtime_none)" # FINDING 4 from the other side: the root's times are reachable only off # `ole.root', because the NAME does not resolve. check_eq $P/getctime-root-name-exc "OSError" "$(pv getctime_root_name_exc)" # FINDING 3. `filetime2datetime' is absent from the submodule's `__all__', so it # is NOT reachable as `olefile.filetime2datetime'. Both halves are pinned: the # absence, and the value the submodule's copy returns for the encoded FILETIME. check_eq $P/filetime2datetime-exported "no" "$(pv filetime2datetime_exported)" check_eq $P/filetime2datetime "2024-06-15 12:34:56" "$(pv filetime2datetime)" # --------------------------------------------------- the three input shapes --- # The SAME container parsed from a path, from a BYTES BLOB and from a FILE-LIKE # OBJECT gives the same tree and the same bytes. Three entry points, one parser. check_eq $P/bytes-listdir "BrintosStream" "$(pv bytes_listdir)" check_eq $P/bytes-stream-matches "yes" "$(pv bytes_stream_matches)" check_eq $P/fileobj-listdir "BrintosStream" "$(pv fileobj_listdir)" check_eq $P/fileobj-stream-matches "yes" "$(pv fileobj_stream_matches)" # The CONTEXT-MANAGER arm closes on exit, and the handle is dead afterwards. check_eq $P/ctx-listdir "BrintosStream" "$(pv ctx_listdir)" check_eq $P/ctx-closed-exc "OSError" "$(pv ctx_closed_exc)" # `OleFileIONotClosed' is a RuntimeWarning, not an exception class -- so a # handle dropped without `close()' warns and does not fail. check_eq $P/notclosed-is-warning "yes" "$(pv notclosed_is_warning)" # ----------------------------------------------------------- write_stream ---- # THE ROUND TRIP, asserted from BOTH sides. Through olefile: reopen and the new # bytes are there, at the same size, with the tree and the timestamps unmoved. check_eq $P/write-mode-flag "True" "$(pv write_mode_flag)" check_eq $P/rewritten-len "4096" "$(pv rewritten_len)" check_eq $P/rewritten-matches "yes" "$(pv rewritten_matches)" check_eq $P/rewritten-size "4096" "$(pv rewritten_size)" check_eq $P/rewritten-listdir "BrintosStream" "$(pv rewritten_listdir)" check_eq $P/rewritten-ctime "2023-12-01 00:00:00" "$(pv rewritten_ctime)" # And from OUTSIDE the parser, which is the half a re-read cannot vouch for: the # file is the same length, everything before the eight data sectors is # byte-identical, the data sectors carry exactly the new payload, and there is # nothing after them. `write_stream' is documented as an IN-PLACE overwrite and # this is what in-place means. check_eq $P/rw-total-size "5632" "$(pv rw_total_size)" check_eq $P/rw-prefix-intact "yes" "$(pv rw_prefix_intact)" check_eq $P/rw-data-replaced "yes" "$(pv rw_data_replaced)" check_eq $P/rw-suffix-intact "yes" "$(pv rw_suffix_intact)" # THE THREE REFUSALS. A shorter or longer replacement is a ValueError -- olefile # cannot resize a stream, so it declines rather than reallocating; an unknown # name is the usual OSError; and a container opened READ-ONLY answers # `io.UnsupportedOperation', which is a subclass of both OSError and ValueError # and is therefore worth pinning by name rather than by either base. check_eq $P/write-short-exc "ValueError" "$(pv write_short_exc)" check_eq $P/write-long-exc "ValueError" "$(pv write_long_exc)" check_eq $P/write-absent-exc "OSError" "$(pv write_absent_exc)" check_eq $P/write-readonly-exc "UnsupportedOperation" "$(pv write_readonly_exc)" # AND NONE OF THEM TOUCHED THE FILE. A refusal that had already written half a # sector would still raise, and would still pass every case above. check_eq $P/rw-unchanged-after-refusals "yes" "$(pv rw_unchanged_after_refusals)" # ------------------------------------------------------------- corruption ---- # FINDING 1, AND THE ONE THIS PORT'S OWN TICKET GOT WRONG BEFORE IT WAS # MEASURED. The ticket predicted that a FAT entry pointing at itself would # "raise rather than loop". It does neither. olefile bounds the walk by # `nb_sectors' computed from the DECLARED size, so the self-loop is not # infinite -- and because the bounded walk returns exactly the declared 4096 # bytes, no length check fires either. There is no exception at ANY defect level # and `parsing_issues' is EMPTY: the caller gets the stream's FIRST data sector # repeated eight times, silently, and cannot tell. check_eq $P/loop-default-exc "none" "$(pv loop_default_exc)" check_eq $P/loop-default-len "4096" "$(pv loop_default_len)" check_eq $P/loop-default-matches "no" "$(pv loop_default_matches)" check_eq $P/loop-default-issues "0" "$(pv loop_default_issues)" check_eq $P/loop-strict-exc "none" "$(pv loop_strict_exc)" check_eq $P/loop-open-exc "none" "$(pv loop_open_exc)" check_eq $P/loop-open-strict-exc "none" "$(pv loop_open_strict_exc)" # WHAT THE WRONG BYTES ACTUALLY ARE. Without this the finding reads as "some # wrong data", which is not something a later reader can check a release against. check_eq $P/loop-is-sector-repeated "yes" "$(pv loop_is_sector_repeated)" # A DIRECTORY ENTRY WHOSE START SECTOR IS OUT OF RANGE, and finding 2 with it. # The container OPENS at both defect levels -- the directory is parsed without # validating the sector against the FAT's length -- and the failure appears only # when the stream is read. At the DEFAULT level that read returns ZERO BYTES and # appends two entries to `parsing_issues'; at DEFECT_INCORRECT it raises # OleFileError. Both are measured, because the difference is the finding. check_eq $P/oor-open-exc "none" "$(pv oor_open_exc)" check_eq $P/oor-open-strict-exc "none" "$(pv oor_open_strict_exc)" check_eq $P/oor-default-exc "none" "$(pv oor_default_exc)" check_eq $P/oor-default-len "0" "$(pv oor_default_len)" check_eq $P/oor-default-issues "2" "$(pv oor_default_issues)" check_eq $P/oor-default-issue0 "OleFileError" "$(pv oor_default_issue0)" check_eq $P/oor-strict-exc "OleFileError" "$(pv oor_strict_exc)" # A TRUNCATED CONTAINER -- header, FAT and directory survive, the data sectors # do not. Same shape as the case above and the same disposition: it opens, and # the default level returns 512 of the declared 4096 bytes with three recorded # issues rather than an error. check_eq $P/trunc-open-exc "none" "$(pv trunc_open_exc)" check_eq $P/trunc-open-strict-exc "none" "$(pv trunc_open_strict_exc)" check_eq $P/trunc-default-exc "none" "$(pv trunc_default_exc)" check_eq $P/trunc-default-len "512" "$(pv trunc_default_len)" check_eq $P/trunc-default-issues "3" "$(pv trunc_default_issues)" check_eq $P/trunc-strict-exc "OleFileError" "$(pv trunc_strict_exc)" # THE ONE MALFORMATION olefile TREATS AS FATAL, and the reason the three above # are the interesting ones: a wrong SIGNATURE is refused at OPEN, at every # defect level, with NotOleFileError. So the sniff and the parser agree about # the header and disagree about everything after it. check_eq $P/badsig-open-exc "NotOleFileError" "$(pv badsig_open_exc)" check_eq $P/empty-open-exc "NotOleFileError" "$(pv empty_open_exc)" # FINDING 3. Neither error class is reachable from the package namespace: the # submodule's `__all__' names `OleFileIONotClosed' and nothing else from the # error tree, so a caller who wants to catch olefile's own errors BY NAME must # reach through `olefile.olefile'. Catching IOError works, which is what the # hierarchy below is for -- and is why this is a wart rather than a defect. check_eq $P/olefileerror-exported "no" "$(pv olefileerror_exported)" check_eq $P/notolefileerror-exported "no" "$(pv notolefileerror_exported)" check_eq $P/notolefileerror-is-olefileerror "yes" "$(pv notolefileerror_is_olefileerror)" check_eq $P/olefileerror-is-ioerror "yes" "$(pv olefileerror_is_ioerror)" # --------------------------------------------------------------- metadata ---- # THE SUMMARY-INFORMATION STREAMS THIS FIXTURE DOES NOT CARRY. `get_metadata()' # returns an OleMetadata whose every attribute is None rather than raising, # which is the answer a caller gets for any container without them -- most # Thumbs.db files, for one. Asserted rather than skipped: a release that started # raising instead would be a behaviour change this suite should see. check_eq $P/metadata-type "OleMetadata" "$(pv metadata_type)" check_eq $P/metadata-title-none "yes" "$(pv metadata_title_none)" check_eq $P/metadata-author-none "yes" "$(pv metadata_author_none)" check_eq $P/metadata-codepage-none "yes" "$(pv metadata_codepage_none)" # The two attribute tables are pinned by SIZE -- 19 SummaryInformation # properties and 28 DocumentSummaryInformation ones -- so a release that grew or # dropped one is visible without this suite carrying either list. check_eq $P/metadata-summary-attrs "19" "$(pv metadata_summary_attrs)" check_eq $P/metadata-docsum-attrs "28" "$(pv metadata_docsum_attrs)" rm -rf "$W" :