Skip to content

fix(outlook): strip NUL terminators from Unicode MSG string streams - #2738

Open
陈志谦 (simpleqt) wants to merge 1 commit into
microsoft:mainfrom
simpleqt:fix/outlook-msg-unicode-nul-terminators
Open

陈志谦 (simpleqt) wants to merge 1 commit into
microsoft:mainfrom
simpleqt:fix/outlook-msg-unicode-nul-terminators

Conversation

@simpleqt

Copy link
Copy Markdown

The bug

OutlookMsgConverter._get_stream_data decodes the PT_UNICODE string streams (__substg1.0_<tag>001F) and only calls str.strip():

return data.decode("utf-16-le").strip()

Some .msg writers terminate or pad a string stream with NULs, and str.strip() keeps NULs, so the terminator survives as a character. Converting such a message emits U+0000 at the end of every header value, the body and the document title:

before after
Subject 'Confirmacion de la reunion\x00' 'Confirmacion de la reunion'
NULs in the output 4 0
a property holding only its terminator reported as the value '\x00' omitted

The non-Unicode path already handles this - _get_ansi_stream_data does data.rstrip(b"\x00"), merged in #2462, and its tests treat b"\x00" / b"\x00\x00" as expected input. The Unicode path was simply missed. Per [MS-OXMSG] 2.1.4.2.2 the string values in these streams end with the NULL terminator, so the input is valid and the output should not carry it.

Fix

def _strip_terminators(text: str) -> str:
    """Remove the NUL terminators or padding some writers append to a stream."""
    return text.rstrip("\x00").strip()

used in place of the bare .strip().

The bytes themselves cannot simply be stripped the way _get_ansi_stream_data does: in a UTF-16 stream the last byte is the high byte of the final character, so data.rstrip(b"\x00") would corrupt the text. Stripping on the decoded string is safe and keeps the two paths equivalent. Removing the terminator before the strip is deliberate, so a value that holds nothing but terminators reads as empty exactly as it does for a non-Unicode message.

Testing

packages/markitdown/tests/test_outlook_msg.py gains test_unicode_terminators_are_removed and test_empty_unicode_properties_are_omitted, both parametrized over b"\x00\x00" and b"\x00\x00\x00\x00", mirroring the existing ANSI cases and comparing the whole converted markdown / title:

  • on the untouched base (21d9c1d, with only the test file copied in and markitdown.__file__ verified from that worktree): 4 failed, 42 passed - the 4 failures are exactly the new cases, and every pre-existing test in the module passes
  • with the fix: 46 passed
  • the full packages/markitdown/tests suite is unchanged apart from the 4 new cases: 1 failed, 1103 passed on base vs 1 failed, 1107 passed with the fix, the single failure being a pre-existing environmental one (test_speech_transcription cannot resolve a host - there is no DNS on this machine)
  • black at the version .pre-commit-config.yaml pins (26.10.1): 2 files would be left unchanged
  • mypy (the repo's configured check script) output is byte-identical before and after, with none of its 30 pre-existing errors in either changed file

_get_stream_data has exactly one caller, _get_property_data, which serves From/To/Subject and the body; nothing else in the repository references it, and the ANSI and ISO-2022-JP paths are untouched. No existing test asserted that a Unicode NUL is retained - every NUL assertion in the suite is on 001E/ANSI streams.

AI assistance: this change was produced with AI assistance (an autonomous coding agent); the analysis, the reproduction, the before/after test runs and the lint runs were executed locally, and a human reviewed the result before submission.

Some writers terminate or pad a string stream with NULs, and the non-Unicode
path removes them before decoding. The Unicode path does not: there a
terminator is a UTF-16 code unit, which decodes to a NUL character that
str.strip() keeps. Converting such a message left a NUL at the end of every
header value, of the body and of the document title, and a property holding
nothing but its terminator was reported as a value ("\x00") instead of being
omitted.

Remove the terminators from the decoded text. That has to happen after
decoding: the last byte of a UTF-16 stream is the high byte of its final
character, so the bytes cannot be stripped the way the 8-bit streams are.

Add regression coverage mirroring the existing ANSI terminator and
empty-property cases.

Signed-off-by: simpleqt <2015900818@qq.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 16:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@simpleqt

Copy link
Copy Markdown
Author

AI assistance

Stated here rather than left implicit: this change was produced with AI assistance - an autonomous coding agent prepared the analysis, the fix, the regression test, and this description. A human reviewed the result before submission.

Disclosing this is now something I do on every submission. I did not do it consistently in earlier contributions, here or elsewhere, and I am going back over them.

This repository has no contributing guide and no AI clause, so nothing here is being corrected; I am stating it because omitting it is what got me into trouble in repositories that do ask.

San-Hsien (SanHsien) pushed a commit to SanHsien/markitdown that referenced this pull request Oct 25, 2026
本 repo 已停用 issues,接續開發報告依規定寫在本 commit 訊息。

# 接續開發報告 2026-10-25

## 一、線索與處理
- open/draft PR:無。PR #1 已關閉未合併,內容已 squash 進 main(bebd625),
  head 分支 ci/job-names-20261024 已不在遠端。
- 非預設分支:只有 claude/gifted-dirac-r9vpm0(本 session 工作分支,內容已 fast-forward 進 main)。
- open issue:無(issues 已停用)。
- main 紅燈 Dependency freshness:ruff 0.17.2。對 E9+F gate 全綠,記入
  .github/dependency-deferrals.json(deferredLatest),未調高下限 → 已轉綠。
- main 紅燈 Upstream check:20 commit/153 PR/40 issue 未審 → 第三輪審查完成,已轉綠。
- HANDOFF.md/TODO.md:不存在。程式碼 TODO/FIXME 7 處皆為上游產品碼,不動。
- REVIEW.md:R-01~R-10 已修,無未修項。
- 2026-09-30 待辦 microsoft#2506:觸發條件成立(上游整併測試依賴它),已採用(4a377f9)。

## 二、做了什麼(bebd625..本 commit)
- 上游 commit:cherry-pick -x 本輪 20 筆中 19 筆(9a15f66..e211f83),microsoft#2731 部分採用(3e440d7),
  補採 microsoft#2506(4a377f9)。
- 例外移植上游 OPEN PR,皆先重現缺陷、附 PR 測試:
  - microsoft#2674(1029094):短 UTF-8 文字被誤判 UTF-16 亂碼。
  - microsoft#2742(9947ed1):UTF-8 BOM 殘留。
  - microsoft#2738(38a2ae6):Outlook MSG 尾端 NUL。
  - microsoft#2620(0d1140c):大寫副檔名音訊不轉譯。
- 依賴(c561006):
  - requirements 跟隨上游 magika/mammoth/certifi,補列 ocr 漏的 PyMuPDF、Pillow。
  - dependabot 補 ocr 與 sample-plugin 目錄。
  - ruff 延後。
- 文件(8f1db57、3382745、b618bc9、本 commit):
  - microsoft#1913 英文段落併入 README.en.md。
  - 繁中 README text_content → markdown。
  - DECISIONS/UPSTREAM/baseline 推進水位。
- CI(d4c2b7a):移除 microsoft#2586 帶進 tests.yml 的 Linux 專用 apt-get 步驟(R-09)。
- 驗證:
  - 每批推送前由全新審查員審 diff 並等 Windows CI 完成(一次 NO-SHIP,修正後重審 ship)。
  - 維護 gate 與 Windows CI py3.10–3.14 全綠。
  - 產品測試(Linux 雲端)1312 passed/12 skipped/2 failed,2 個失敗皆因沙箱無對外網路。

## 三、上游結論
- 水位:commit 7482c09、PR microsoft#2765、issue microsoft#2762。
- commit:採用 18、部分採用 1(microsoft#2731 ARM/pre-commit CI 不適用),microsoft#2733 版本 bump 不適用。
- PR:
  - MERGED 12,例外移植 4。
  - 其餘 OPEN 不 fork-ahead,等上游合併。
  - 無 merge ref 12 筆多為重複,不採。
- issue:已修 2(microsoft#2646、microsoft#2719),16 筆對應 OPEN PR,其餘為不適用、觸發條件或垃圾。
- 逐筆見 docs/DECISIONS.md 2026-10-25。

## 四、刻意不做(理由/觸發條件)
- microsoft#2756 ZIP 檔名:會把 Big5「臺灣報告.pdf」解成亂碼(誤判為 shift_jis_2004)。
  觸發:上游合併時先補 cp950 測試,或改為優先 metadata_encoding/locale。
- microsoft#2621/microsoft#2622 SSRF 政策:牽動 MCP/URI 擷取。
  觸發:上游合併,或本 fork 對外提供 MCP HTTP 服務。
- microsoft#2679/microsoft#2723 cp1252 偵測:可能影響 CJK 舊碼頁。觸發:上游合併後跑 CJK charset 測試。
- microsoft#2407 youtube-transcript-api ~=1.2.3:維持本 fork >=1.0.0+freshness-hold。
- microsoft#2740 Python 3.15:上游 CI 加 3.15 時同步 ci.yml。

## 五、需要維護者決定
1. 雲端 session 對 microsoft/markitdown 無 API 權限(只能匿名 git 讀取)。
   本輪 PR/issue 清單取自本 fork Upstream check log,狀態以 refs/pull/*/merge 判斷。
2. 產品程式變動只在 Linux 跑過;ci.yml 只跑維護 gate。請在 Windows 11 本機跑
   pwsh -NoProfile -File tools\test_product.ps1 -AllPackages。
3. 「不 fork-ahead」政策的 4 筆例外移植;若不同意可 revert 1029094、9947ed1、38a2ae6、0d1140c。

STATUS: NEEDS-MAINTAINER Windows 11 本機跑 test_product.ps1 -AllPackages;確認是否接受 4 筆上游 open PR 例外移植

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014CWBckUdMZcCAsc15iaJtm
San-Hsien (SanHsien) pushed a commit to SanHsien/markitdown that referenced this pull request Oct 25, 2026
本 repo 已停用 issues,接續開發報告依規定寫在本 commit 訊息。

# 接續開發報告 2026-10-25

## 一、線索與處理
- open/draft PR:無。PR #1 已關閉未合併,內容已 squash 進 main(bebd625),
  head 分支 ci/job-names-20261024 已不在遠端。
- 非預設分支:只有 claude/gifted-dirac-r9vpm0(本 session 工作分支,內容已 fast-forward 進 main)。
- open issue:無(issues 已停用)。
- main 紅燈 Dependency freshness:ruff 0.17.2。對 E9+F gate 全綠,記入
  .github/dependency-deferrals.json(deferredLatest),未調高下限 → 已轉綠。
- main 紅燈 Upstream check:20 commit/153 PR/40 issue 未審 → 第三輪審查完成,已轉綠。
- HANDOFF.md/TODO.md:不存在。程式碼 TODO/FIXME 7 處皆為上游產品碼,不動。
- REVIEW.md:R-01~R-10 已修,無未修項。
- 2026-09-30 待辦 microsoft#2506:觸發條件成立(上游整併測試依賴它),已採用(4a377f9)。

## 二、做了什麼(bebd625..本 commit)
- 上游 commit:cherry-pick -x 本輪 20 筆中 19 筆(9a15f66..e211f83),microsoft#2731 部分採用(3e440d7),
  補採 microsoft#2506(4a377f9)。
- 例外移植上游 OPEN PR,皆先重現缺陷、附 PR 測試:
  - microsoft#2674(1029094):短 UTF-8 文字被誤判 UTF-16 亂碼。
  - microsoft#2742(9947ed1):UTF-8 BOM 殘留。
  - microsoft#2738(38a2ae6):Outlook MSG 尾端 NUL。
  - microsoft#2620(0d1140c):大寫副檔名音訊不轉譯。
- 依賴(c561006):
  - requirements 跟隨上游 magika/mammoth/certifi,補列 ocr 漏的 PyMuPDF、Pillow。
  - dependabot 補 ocr 與 sample-plugin 目錄。
  - ruff 延後。
- 文件(8f1db57、3382745、b618bc9、本 commit):
  - microsoft#1913 英文段落併入 README.en.md。
  - 繁中 README text_content → markdown。
  - DECISIONS/UPSTREAM/baseline 推進水位。
- CI(d4c2b7a):移除 microsoft#2586 帶進 tests.yml 的 Linux 專用 apt-get 步驟(R-09)。
- 驗證:
  - 每批推送前由全新審查員審 diff 並等 Windows CI 完成(一次 NO-SHIP,修正後重審 ship)。
  - 維護 gate 與 Windows CI py3.10–3.14 全綠。
  - 產品測試(Linux 雲端)1312 passed/12 skipped/2 failed,2 個失敗皆因沙箱無對外網路。

## 三、上游結論
- 水位:commit 7482c09、PR microsoft#2765、issue microsoft#2762。
- commit:採用 18、部分採用 1(microsoft#2731 ARM/pre-commit CI 不適用),microsoft#2733 版本 bump 不適用。
- PR:
  - MERGED 12,例外移植 4。
  - 其餘 OPEN 不 fork-ahead,等上游合併。
  - 無 merge ref 12 筆多為重複,不採。
- issue:已修 2(microsoft#2646、microsoft#2719),16 筆對應 OPEN PR,其餘為不適用、觸發條件或垃圾。
- 逐筆見 docs/DECISIONS.md 2026-10-25。

## 四、刻意不做(理由/觸發條件)
- microsoft#2756 ZIP 檔名:會把 Big5「臺灣報告.pdf」解成亂碼(誤判為 shift_jis_2004)。
  觸發:上游合併時先補 cp950 測試,或改為優先 metadata_encoding/locale。
- microsoft#2621/microsoft#2622 SSRF 政策:牽動 MCP/URI 擷取。
  觸發:上游合併,或本 fork 對外提供 MCP HTTP 服務。
- microsoft#2679/microsoft#2723 cp1252 偵測:可能影響 CJK 舊碼頁。觸發:上游合併後跑 CJK charset 測試。
- microsoft#2407 youtube-transcript-api ~=1.2.3:維持本 fork >=1.0.0+freshness-hold。
- microsoft#2740 Python 3.15:上游 CI 加 3.15 時同步 ci.yml。

## 五、需要維護者決定
1. 雲端 session 對 microsoft/markitdown 無 API 權限(只能匿名 git 讀取)。
   本輪 PR/issue 清單取自本 fork Upstream check log,狀態以 refs/pull/*/merge 判斷。
2. 產品程式變動只在 Linux 跑過;ci.yml 只跑維護 gate。請在 Windows 11 本機跑
   pwsh -NoProfile -File tools\test_product.ps1 -AllPackages。
3. 「不 fork-ahead」政策的 4 筆例外移植;若不同意可 revert 1029094、9947ed1、38a2ae6、0d1140c。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014CWBckUdMZcCAsc15iaJtm
STATUS: NEEDS-MAINTAINER Windows 11 本機跑 test_product.ps1 -AllPackages;確認是否接受 4 筆上游 open PR 例外移植
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants