Skip to content

Read task Notes when opening a .mpp (#455) - #519

Merged
yeroo merged 7 commits into
mainfrom
issue-455-bug-opening-a-mpp-drops-task-not
Sep 28, 2026
Merged

yeroo merged 7 commits into
mainfrom
issue-455-bug-opening-a-mpp-drops-task-not

Conversation

@yeroo

@yeroo yeroo commented Sep 28, 2026

Copy link
Copy Markdown
Owner

What and why

Opening a .mpp dropped task Notes, one of the task elements #417 made Task keep, so a .mpp opened and saved as .yppx/.xml lost them.

Current-layout .mpp files store a task's notes as RichEdit RTF under Var2Data key 0x000F (found by diffing corpus steps 06 → 07; the block is the RTF followed by \r\n and a NUL). This PR:

  • decodes that key in taskdecode::var_fields into MppTaskFields.notes, and import_tasks copies it into Task.notes;
  • adds mppread/src/rtf.rs, a dependency-free RTF → plain-text reader producing exactly the text Project's MSPDI export writes (final \par dropped, CRLF paragraphs). It hides non-text destinations (font/colour tables, pictures, objects, \bin payloads, bullet pntext, fields, headers…), honours \uc/\u, and decodes \'hh and raw 8-bit text through the current font's \cpg/\fcharset or \ansicpg;
  • adds mppread/src/rtf_codepage.rs, the Windows single-byte code pages 874 and 1250–1258, generated from MultiByteToWideChar by corpus/tools/gen_rtf_codepages.py; Symbol-charset bytes map to U+F000+byte;
  • refuses a note with "invalid notes for UID n" when its RTF is malformed, uses a DBCS/unknown code page for escaped bytes, or decodes to a character XML 1.0 cannot hold (consistent with how task names are validated);
  • documents key 0x000F in corpus/mpp/README.md and adds Notes to PROJECT.md's list of fields that survive an import.

Not in this PR (no corpus case shows where Project stores them — tracked in a follow-up issue): Contact, WBSLevel, SubprojectName, PreLeveledStart/Finish, StatusManager, CommitmentStart/Finish, IsPublished (every snapshot row matches !summary && active, so no bit can be told apart from a summary/active marker without a Publish toggle case), non-default DisplayAsSummary/CommitmentType, and Notes on the legacy/MPP12 decode path.

Closes #455

Tests

  • oracle_corpus: same!(notes) — every current-layout .mpp pair's decoded and imported Notes equal Project's MSPDI export (incl. step 07's First line.\r\nSecond line — unicode ✓ and «quotes».).
  • taskdecode: the verbatim 314-byte corpus note decodes, imports, and survives write_mspdi → read_mspdi; corrupt RTF refuses with the UID.
  • rtf.rs unit tests: RichEdit notes, bullets, pictures/objects/\bin, code pages and charsets (incl. brace-less font tables and nested \fontfile), \uc fallbacks, control symbols, malformed and XML-illegal input.
  • Whole suite on the PR head: see the auto-merge note below. overalloc::tests::project_assignment_and_resource_offsets is skipped locally: with only the fetched snapshots/ corpus present it finds 47 < 50 Project pairs — pre-existing and unrelated (follow-up filed); CI has no corpus, where it returns early.

Review record

Plan agreed with Codex at v2 (v1 dropped IsPublished decoding for lack of a toggle case, and pinned the note block's \r\n\0 tail). Five revmux rounds:

  • r1: 3 Major (picture/\bin payloads leaked or refused, \fcharset ignored, non-1252 locales refused the file), 4 Minor (fallback skipping, backslash-newline, test helper, docs) — all fixed.
  • r2: 2 Major (bullet lists refused, \fcharset0 used \ansicpg), 4 Minor (\plain, unknown control symbols, Python vs Windows code-page tables, README escape), 3 Immaterial (\cpg, header check, duplicated lexer) — all fixed.
  • r3: 1 Major (Thai cp874), 1 Minor (raw 8-bit text), 1 Immaterial (nested \fontfile\cpg) — all fixed.
  • r4: 1 Minor (brace-less font tables, a regression from r3's fix) — fixed.
  • r5: 2 Minor (nested \fontfile in a brace-less entry, XML-illegal characters) — fixed and verified by the planner's direct diff read.

No finding was disputed or deferred.

yeroo and others added 6 commits September 28, 2026 15:31
Current-layout .mpp files keep a task's notes as RichEdit RTF under
VarMeta key 0x000F. var_fields now decodes that block to the plain text
Project's MSPDI export writes (a new dependency-free RTF reader), and
import_tasks carries it into Task.notes, so an opened .mpp saved as
.yppx/.xml keeps its notes. The oracle comparison checks Notes on every
current-layout pair.

The other #417 task elements stay unread: no corpus case shows where
Project stores them.

Committed by the planner on the implementer's behalf: its sandbox denies
writes to .git.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#455 r1)

Picture, object and other non-text destinations are hidden and \binN
payloads are skipped, so embedded images no longer leak hex into Notes
or refuse the file. Escaped bytes are decoded through the selected
font's \fcharset or the document's \ansicpg, with tables for Windows
code pages 1250-1258, instead of always as cp1252. \uc fallbacks skip
control words and stop at a brace, and backslash-newline is a paragraph
mark. The notes test uses add_var; the corpus README and PROJECT.md
record the Notes key.

Committed by the planner on the implementer's behalf: its sandbox denies
writes to .git.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RichEdit bullet markers (pntext/listtext) are hidden and Symbol-charset
escapes map to U+F000+byte, so a bulleted note no longer refuses the
file. \fcharset0 is cp1252 and \fcharset1 follows \ansicpg; a font's
\cpgN wins over its charset; \plain restores the \deff font. Unknown
control symbols are ignored. The code-page tables are regenerated from
MultiByteToWideChar by the new corpus/tools/gen_rtf_codepages.py. The
header must be \rtfN, and parse and fallback skipping share one lexer
with strict hex digits.

Committed by the planner on the implementer's behalf: its sandbox denies
writes to .git.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Code page 874 joins the generated tables and \fcharset222 maps to it,
so a Thai note no longer refuses the file. Unescaped high-bit bytes are
decoded through the current font's code page like \'hh escapes, and a
font's \fcharset/\cpg is taken only at its definition's own depth, not
from a nested \fontfile.

Committed by the planner on the implementer's behalf: its sandbox denies
writes to .git.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The r3 depth gate only accepted \fcharset/\cpg one group below
\fonttbl, so a font table written without per-entry braces lost its
charsets and decoded under \ansicpg. One in_font_entry helper now
accepts the fonttbl group and one level below it, still excluding a
nested \fontfile.

Committed by the planner on the implementer's behalf: its sandbox denies
writes to .git.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…notes (#455 r5)

A hidden or \* destination opened inside the font table no longer counts
as a font entry, so a nested \fontfile\cpg cannot override a brace-less
entry's \fcharset. A decoded note holding a character outside XML 1.0
Char (C0 controls other than TAB/CR/LF, U+FFFE, U+FFFF) is refused like
a task name with control characters, since saving it would write
ill-formed MSPDI.

Committed by the planner on the implementer's behalf: its sandbox denies
writes to .git.

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in mppread/src/lib.rs: both sides added private modules
(rtf/rtf_codepage here, rscdecode/tabledecode on main); kept all four.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 93.36188% with 31 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mppread/src/rtf.rs 93.85% 25 Missing ⚠️
mppread/src/rtf_codepage.rs 71.42% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

@yeroo
yeroo merged commit 0fd20f1 into main Sep 28, 2026
11 checks passed
@yeroo
yeroo deleted the issue-455-bug-opening-a-mpp-drops-task-not branch September 28, 2026 13:40
@yeroo

yeroo commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Merged automatically (auto-merge and full autonomy are on for this checkout):

Follow-ups:

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.

bug: opening a .mpp drops task Notes and the other task elements #417 keeps

2 participants