diff --git a/corpus/media-edit/README.md b/corpus/media-edit/README.md new file mode 100644 index 0000000000000000000000000000000000000000..38c9fc6c77e8e50438af8e2d58729ef30529091d --- /dev/null +++ b/corpus/media-edit/README.md @@ -0,0 +1,17 @@ +# Editing next to a recording + +`candidate/Features.one` is the writer's output for +`editing_a_recorded_page_keeps_the_recording_and_its_annotation` in +`crates/onestore/tests/page_media.rs`: the native section of +`corpus/m6/native-features-01` with one plain paragraph appended to the page +"Files and recording", where OneNote 2010 recorded `silence.wav` and +annotated a paragraph with the moment 500 ms. The recording attachment +(`page::Attachment::recording`) and the annotation +(`PageParagraph::media`) are read from the page and kept through the edit; +a model that adds an annotation or drops the recording refuses to write. + +`cold/` is a fresh OneNote 2010 read: `MediaFile`, `MediaReference` and +`MediaIndex` name the same recording and moment as before the edit, next to +the added paragraph. `tools/test_media_edit.py` checks this without a VM. +Regenerate with `ONESTORE_MEDIA_EXPORT` set to a new directory while running +the test, then cold-open with `--expected-pages 13 --collect-notebook`. diff --git a/corpus/media-edit/candidate/Features.one b/corpus/media-edit/candidate/Features.one new file mode 100644 index 0000000000000000000000000000000000000000..713b4cc25604250b16cf5563975c219c0889538a Binary files /dev/null and b/corpus/media-edit/candidate/Features.one differ diff --git a/corpus/media-edit/candidate/Open Notebook.onetoc2 b/corpus/media-edit/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..c7273fd1d4b80a77fecd7a9bddd97c2d8292167e Binary files /dev/null and b/corpus/media-edit/candidate/Open Notebook.onetoc2 differ diff --git a/corpus/media-edit/candidate/expected-count.txt b/corpus/media-edit/candidate/expected-count.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca7bf83ac53a27a2a914bed25e1a07478dd8ef47 --- /dev/null +++ b/corpus/media-edit/candidate/expected-count.txt @@ -0,0 +1 @@ +13 \ No newline at end of file diff --git a/corpus/media-edit/cold/commands.jsonl b/corpus/media-edit/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..b0e045fa46bb1a64d9cbe1d3bd59e196b7b12113 --- /dev/null +++ b/corpus/media-edit/cold/commands.jsonl @@ -0,0 +1,3 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-7A9FE25A -ExpectedPages 13", "exit": 0, "stdout": "Read 1 sections and 13 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/media-edit/cold/machine.json b/corpus/media-edit/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..c513ec9ff9b7366ecb6204e5318f4060e031de14 --- /dev/null +++ b/corpus/media-edit/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-7a9fe25a", "hostname": "ONE-M6-7A9FE25A"} diff --git a/corpus/media-edit/cold/notebook/Features.one b/corpus/media-edit/cold/notebook/Features.one new file mode 100644 index 0000000000000000000000000000000000000000..293f5d79a89ac3a985b34caf2586c7a5ba8c9ba9 Binary files /dev/null and b/corpus/media-edit/cold/notebook/Features.one differ diff --git a/corpus/media-edit/cold/notebook/Open Notebook.onetoc2 b/corpus/media-edit/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..c78460b407cbe0b72d1173cdff042b941bfe559a Binary files /dev/null and b/corpus/media-edit/cold/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/media-edit/cold/notebook/expected-count.txt b/corpus/media-edit/cold/notebook/expected-count.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca7bf83ac53a27a2a914bed25e1a07478dd8ef47 --- /dev/null +++ b/corpus/media-edit/cold/notebook/expected-count.txt @@ -0,0 +1 @@ +13 \ No newline at end of file diff --git a/corpus/media-edit/cold/read/4cff0039dbdf53a59f5f4e4592c29a5297ea3b7acce24415c0ae83c36c6bd7c2.attachment b/corpus/media-edit/cold/read/4cff0039dbdf53a59f5f4e4592c29a5297ea3b7acce24415c0ae83c36c6bd7c2.attachment new file mode 100644 index 0000000000000000000000000000000000000000..a959c053358a06abc072d3492e7a86d2e712d804 Binary files /dev/null and b/corpus/media-edit/cold/read/4cff0039dbdf53a59f5f4e4592c29a5297ea3b7acce24415c0ae83c36c6bd7c2.attachment differ diff --git a/corpus/media-edit/cold/read/56d4af65701c26df20bd4021eda95b6e830348ce3a746086079fe89285548dc9.attachment b/corpus/media-edit/cold/read/56d4af65701c26df20bd4021eda95b6e830348ce3a746086079fe89285548dc9.attachment new file mode 100644 index 0000000000000000000000000000000000000000..96260477f0ca60b1605e2971d8cdc146b8b4e02b Binary files /dev/null and b/corpus/media-edit/cold/read/56d4af65701c26df20bd4021eda95b6e830348ce3a746086079fe89285548dc9.attachment differ diff --git a/corpus/media-edit/cold/read/environment.json b/corpus/media-edit/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..19f357038cc1c48596167daa2fa96e692d2761f7 --- /dev/null +++ b/corpus/media-edit/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-7A9FE25A", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/media-edit/cold/read/hierarchy.xml b/corpus/media-edit/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..e7f0b76c346f33d56bb32995acfd6722a32e6e1e --- /dev/null +++ b/corpus/media-edit/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/media-edit/cold/read/page-000.xml b/corpus/media-edit/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..39048fca3bbfeb051bce15d8758c2f021e375015 --- /dev/null +++ b/corpus/media-edit/cold/read/page-000.xml @@ -0,0 +1,300 @@ + +iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAAXNSR0IArs4c6QAAAARnQU1BAACx +jwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAEImSURBVHheDbiBV2GL+v+/f9V1q+uk46TjpDFp +TBqDMUhoh3Zoh3Zoh3Zoh3YYgzE0Jo1J46TjpOOk61Yf32pZ1bKqZVXLqpZV/W2/8y8863nez/v1 +AgAAaAL+vxagiQQ0twIt7cC/yACpA/h3J9BKBdq6gPZu4D90gNwD/NQLdDABSh/Q2Q/8zAaoA8Av +g0AXF6DxgG4B8KsQoIuA3yRAjxRgyIBeBfACBJhK4KUa6IMAlgbo1wGvYICtB14bgQEE4JiAQQvw +BgW4VuCtHeBhAN8BCFzAOxwQuoH3XkBEAGIfIAkAQ0FAGgKGI4AsCshjgGIFGEkA4CowugYoU4Bq +HVBvAGMZANoExrcATQ7QbgO6HWCiAMC7wOQeoC8Bhn3AeAhMlQHkCJg+AUwVwHwKWM6BmSqAXgCz +V4C1BtiuAfstMFcHsDtg/gFwNADnI+B6BhYAoPn/a2puamluJjW3tDb/q72ZRG7+d0dza2dzG7W5 +vav5P93NZHrzTz3NHb3NFGZzZ1/zz/3NVHbzLwPNXYPNNG5zN6/5V0EzXdj8m6i5R9LMkDb3yppf +KJqZYPNLZXOfupkFNfdrml/pmtlw82t984CxmYM0D5qa31iauWjzW2szz97Mx5oFjuZ3rmYh3vze +3SzyNouJZomveSjQLA02D4eaZZFmebRZEWseWWkGE82jq83KtWZVqlm93jy20Qxlmsc3mzVbzdpc +s267eWKnGS40T+426/eaDaVm437z1GEzUm6ePmo2nTSbK82W0+aZ82a02jx70Wy9arbVmu3XzXO3 +zVi9ef6u2fHQ7Gw0ux6bF56bcQAgNTX9MxtSC4n0r1YSqZ30bzKptYPU1klqp5L+00Uid5N+opM6 +ekiUXlInk/RzH4naT/qFTeoaINEGSd1c0q88El1A+k1I6hGRGBJSr5T0QkZiKkgvQVKfksRSk/oh +0isNia0jvYZJA3oSx0gaREhvTCSuhfQWJfGsJL6dJMBI7xwkoYv0HieJ3CSxlyQhSEM+kjRAGg6S +ZCGSPEJSREkjMRK4QhpNkJSrJNUaSZ0ija2ToA3SeIak2SRpt0i6HGlimwTvkCYLJP0uybBHMpZI +U/sk5JA0XSaZjkjmE5KlQpo5JaHnpNkqyXpBsl2R7DXS3DUJuyXN10mOO5LzgeRqkBYeSfgzaREA +2pqb2lpa2v5FaiO1tv27va2V3NbW0dbe2fYfahu5q+2n7rYOehulp62zt+1nZhu1r+2X/rYudhtt +oK17sO1Xbhud1/aboK1H2MYQtfVK2l5I25iytpeKtj6wjaVs61e3vYLa2Jq217q2AbiNo28bNLa9 +Qdq4pra3ljYe2sa3tgnsbe+wNqGj7b2rTYS3id1tEm/bENEm9bUNB9pkwTZ5qE0RaRuJtoGxttGV +NmWiTbXapl5rG0u1Qett4xttmkybdrNNt9U2kWuDt9smd9r0hTbDbptxr22q1Ibst00ftpnKbeaj +NstJ20ylDT1tmz1vs1bbbBdt9qu2uVobdt02f9vmqLc579pcD20LjTb8sW3xuc0NAOSWJvK/Wsgk +EvnfreTWdnIbmdzeQf5PJ5lMJf/URe7oJlPo5M4e8s+9ZCqT/EsfuaufTGOTuwfIvw6S6Vzybzxy +j4DMEJJ7ReQXEjJTSn4pI/cpyCyQ3K8kv1KT2RD5tYY8oCNzYPKgnvzGSOYi5LcmMs9C5qNkgZX8 +zk4WYuT3DrLIRRbjZImbPOQlSwnysI8sC5DlQbIiRB6JkMEoeTRGVq6QVQmyepU8tkaGUuTxdbJm +g6zNkHWb5IktMpwjT26T9TtkQ4Fs3CVP7ZGREnl6n2w6JJvLZMsReeaEjFbIs6dk6znZViXbL8hz +V2SsRp6/Jjtuyc462XVHXngg4w3y4iPZ/Uz2AADlX00UUgvl3yRKayulrZ3STqb8p4NC7qT8RKV0 +dFEo3ZROOuXnHgq1l/ILk9LVR6H1U7rZlF8HKPRBym9cSg+PwhBQeoWUFyIKU0J5KaX0ySgsBaUf +pLxSUthqymuIMqChcHSUQZjyRk/hGilvEQrPROFbKAKU8s5KEdop7zGKyEERuygSnDLkpki9lGGC +IvNR5AGKIkgZCVHACGU0SlHGKKoVijpBGVulQGuU8RRFs07RblB0GcrEJgXeokzmKPptimGHYixQ +pnYpyB5lukQx7VPMhxRLmTJzREFPKLMVivWUYjun2KuUuQsKdkWZr1Ec1xTnLcVVpyzcUfAHymKD +4n6keJ4pXgCgkpqo/26htpKoba3U9nbqf/7ZnA7qT53UDiqV0kXt7Kb+TKdSe6i/9FK7mFRaH7W7 +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 +B583DmKZg/jmwcrWwZfcQWL74OvOwWrhILl7sLZ38K10kNo/+H54sF4+SB8dbJwc/KgcZE4Pfj8/ +2KweZC8Otq4O/qgd5K4P/rw92K4f5O8Odh4O/mocFB4P/n4+2AWAsqSpPNRSlpLKw61lWXtZTi4r +OsojnWWQWh7tKiu7yyp6Wd1THustQ8zyeF9Z01/Wssu6gfLEYBnmlid5Zb2gbBCWjaLylKSMSMvT +srJJUTaDZYuyPKMuo1B5VlO26so2uGzXl+eMZQwpz5vKDkvZiZZd1vKCvYxj5UVH2e0qe/Cy111e +8pYJorzsK/sCZX+wHAiVP0T+0Tnlj7FyaKUcTpQjq+VPa+Voqvx5vRzbKMcz5ZXN8petciJX/rpd +Xt0pJwvltd3yt71yqlT+vl9ePyyny+WNo/KPk3KmUv79tLx5Xs5Wy1sX5T+uyrla+c/r8vZtOV8v +79yV/3ooFxrlvx/Lu8/lIgAcDzUdS1uOh0nHstZjefuxgnw80nEMdh6PUo+VXceq7mM1/Xis5xjq +PR5nHmv6jrX9xzr28cTAMTx4PMk91vOODYJjo/B4SnSMSI6npccm2bFZcWwBj2eUx6j6eBY6tmqO +bbpjO3w8pz/GjMfzyLHDdOy0HLvQ4wXrMW4/XsSO3Y5jj+vYix8vuY8J7/EycezzHfsDx4Hg8YfQ +cTBy/DF6HIodh1eOI4njT6vH0bXjz6nj2PpxfON4JXP8ZfM4sXX8NXe8un2c3DleKxx/2z1O7R1/ +Lx2v7x+nD483ysc/jo4zJ8e/V443T4+z58db1eM/Lo5zV8d/1o63r4/zt8c79eO/7o4LD8d/N453 +H4+Lz8d7AFCRNlWGWyoyUkXeWlG0V0bIFbCjMtpZUVIrqq6KursyRq9APZXx3oqGWdH2VXT9lQl2 +BR6oTA5W9NyKgVcxCipTwgoiqkxLKiZpxSyrWBSVGbCCKiuz6ooVqtg0FbuuMgdXMH1l3lhxIBWn +qeKyVBbQCm6tLNorbqzicVS8rsoSXiHclWVvxUdU/L5KIFD5EKwEQ5WPkUooWgnHKpGVyqdEJbpa ++bxWiaUq8fXKykblS6aS2Kx83aqs5irJ7craTuVboZLarXzfq6yXKun9ysZh5Ue5kjmq/H5S2axU +sqeVrfPKH9VK7qLy51Vlu1bJX1d2bit/1SuFu8rfD5XdRqX4WNl7rvwXAM6Gm85kLWdy0pmi9Wyk +/Qwkn412nCk7z1TUM3XX2Vj3GUQ/G+850/SeaZlnur6zif4zmH02OXCmHzwzcM+MvLMpwRkiPJsW +nZkkZ2bpmUV2NqM4Q8GzWeWZVX1mg87smrM53RkGn83rzxzGMydy5jKdLVjOcPRs0Xrmtp95sDOv +42zJdUbgZ8vuM5/3zE+cBXxnHwJnweDZx9BZKHIWjp5FYmefVs6iibPPq2extbN46mxl/ezLxlki +c/Z182x16yyZO1vbPvu2c5YqnH3fPVvfO0uXzjb2z34cnmXKZ78fnW2enGUrZ1unZ3+cn+WqZ39e +nG1fneVrZzvXZ3/dnhXqZ3/fne0+nBUbZ3uPZ/99PisBQFXWVJW3VBWk6khrFWyvjpKryo6qqrOq +plbHuqpQd3WcXtX0VLW9VR2zOtFXhfurk+yqfqBqGKwaudUpXhURVKeFVZOoapZULdLqjKyKKqqz +YNWqrNrUVTtUndNUMV11Hq469FWnsepCqgumKm6pLqJVt7XqsVe9WHXJUSVc1WW86nNX/d5qgKh+ +8FWDgerHYDUUqoYj1Ui0+ilWja5UPyeqsX9k11p1JVX9sl5NbFS/Zqqrm9XkVnUtV/22XU3tVL8X +quu71fRedaNU/bFfzRxWfy9XN4+q2ZPqVqX6x2k1d179s1rdvqjmr6o7tepf19XCbfXvenX3rlp8 +qO41qv99rJaeq/8DgEt506Wi5XKEdAm2Xo62XyrJl6qOS3Xn5Rj1Euq6HO++1NAvtT2Xut7LCeYl +3Hc52X+pZ18aBi6Ng5dT3EuEdzktuDQJL82iS4vkckZ6icouZxWXVvDSpry0qy/noEtMczmvu3TA +l079pct4uYBc4qbLRculG730WC+99ssl7JJwXC67Ln34pd99GfBefiAug77Lj4HLUPAyHLqMRC4/ +RS+jscvPK5exxGV89XJl7fJL6jKxfvl143I1c5ncvFzbuvyWu0xtX37fuVwvXKZ3Lzf2Ln+ULjP7 +l78fXm6WL7NHl1snl39ULnOnl3+eX25XL/MXlztXl3/VLgvXl3/fXu7WL4t3l3sPl/9tXJYeL//3 +fLkPADVFU22kpQaSaqOtNWV7TUWuqTtqY501iFob76ppumtaek3XU5vorcHM2mRfTd9fM7BrxoHa +1GAN4dameTWToGYW1iyi2oykhkprs7KaVVGzgTW7sjanrmFQbV5Tc+hqTrjm0tcWjDUcqS2aam5L +zYPWvNbakr1GYLVlR83nqvnxWsBd++CtBYnaR18tFKiFg7VIqPYpUotGa59jtdhKLZ6orazWvqzV +/lGBX9drqxu1ZKa2tln7tlVL5Wrft2vrO7V0obaxW/uxV8uUar/v1zYPa9lybeuo9sdJLVep/Xla +2z6v5au1nYvaX1e1Qq3293Vt97ZWrNf27mr/faiVGrX/Pdb2n2sHAHAz0nQDttyMkm6UrTeq9hs1 ++Was4wbqvBmn3mi6brTdNzr6zUTPDdx7M8m80ffdGPpvjOybqYEbZPBmmntj4t2YBTcW4c2M6AaV +3MxKb6yyG5vixg7ezClvMPXNPHTj0Nw4dTcu+GZBf4MbbxaRG7fpxmO58aI3S9Ybwn6zjN34HDd+ +100Av/ngvgl6bz4SNyHfTThwEwnefArdRCM3n6M3sdhNfOVmJXHzZfUmsXbzNXWzun6T3LhZy9x8 +27xJbd18z92sb9+kd242Cjc/dm8yeze/l24292+yhzdb5Zs/jm5yJzd/Vm62T2/y5zc71Zu/Lm4K +Vzd/1252r2+Ktzd79Zv/3t2UHm7+17jZf7w5eL45BIA62FQfbakrSXVVa13dXh8j16GO+nhnXUOt +a7vquu76BL0O99Qne+t6Zt3QVzf216fYdWSgPj1YN3HrZl7dIqjPCOuoqD4rqVuldZusblfU58A6 +pqzPq+sOqO7U1F26+gJcx/X1RWPdjdQ9prrXUl9C64S1vmyv+7C631EPuOof8HrQXf/orYeIethX +jwTqn4L1aKj+OVKPRevxWH1lpf4lUU+s1r+u1VdT9eR6fW2j/i1TT23Wv2/V13P19HZ9Y6f+o1DP +7NZ/36tvlurZ/frWYf2Pcj13VP/zpL5dqedP6zvn9b+q9cJF/e+r+m6tXryu793W/1uvl+7q/3uo +7zfqB4/1w+f6/wHA/WjTvbLlXkW6V7fej7XfQ+T78Y57Tee9lnqv67qf6L6H6feTPff63nsD897Y +dz/Vf4+w76cH7k2D92buvYV3PyO4R4X3s6J7q+TeJr23y+7nFPcYeD+vvHeo753QvUtzv6C7x+H7 +Rf2923jvQe69pvslyz2B3i9b7332ez92H3Dcf3DdB/H7j+77kPc+TNxHfPefAvfR4P3n0H0sch+P +3q/E7r+s3CcS919X71fX7pOp+7X1+28b96nM/ffN+/Wt+3TufmP7/sfOfaZw//vu/ebefbZ0v7V/ +/8fhfa58/+fR/fbJfb5yv3N6/9f5faF6//fF/e7VfbF2v3d9/9/b+1L9/n939/sP9weN+8PH+/97 +vi8DQEPZ1FC1NNSkxlhrA2pvjJMbmo6GtrOhozYmuhpwd2OS3tD3NAy9DSOzMdXXQPob0+yGaaBh +HmxYuI0ZXgMVNGaFDauoYZM07NLGnKyBKRrzYMOhbDjVDRfUWNA0cF1jEW649Q2PseFFGkumBmFp +LKMNn7XhtzcCWOODoxF0NT7ijZC7EfY2IkTjk68RDTQ+BxuxUCMeaaxEG19ijcRK42uisbraSK41 +1lKNb+uN1Ebje6axvvmPJW1s5Bo/thuZncbvhcbmbiO719gqNf7Yb+QOG3+WG9tHjfxJY6fS+Ou0 +UThv/F1t7F40ileNvVrjv9eN0m3jf/XG/l3j4KFx2Gj832Oj/Nz4fwDwpGp6Urc8jZGeoNan8fYn +DflJ2/Gk63yaoD7BXU+T3U96+pOh58nY+zTFfEL6nqb7n0zsJ/PAk2XwaYb7hPKeZgVPVuGTTfRk +lzzNSZ8w2dO84skBPjmVTy710wL0hGueFnVPbvjJo3/yGp+WkCfC9LRsefKhT37rU8D+9AF7Cjqe +PrqeQvhT2P0U8T59Ip6ivqfPgadY8CkeelqJPH2JPiViT19XnlYTT8nVp7W1p2+pp9T60/eNp/XM +U3rzaWPr6UfuKbP99PvO02bhKbv7tLX39EfpKbf/9Ofh03b5KX/0tHPy9FflqXD69Pf50271qXjx +tHf19N/aU+n66X+3T/v1p4O7p8OHp/9rPJUfn/7f89PR/w8QCy14LJdV2gAAAABJRU5ErkJggg== diff --git a/corpus/media-edit/cold/read/page-001.xml b/corpus/media-edit/cold/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..0ec54232e1f3400a55b8b2f40866327732f10047 --- /dev/null +++ b/corpus/media-edit/cold/read/page-001.xml @@ -0,0 +1,21 @@ + +/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a +HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy +MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABAAGADASIA +AhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA +AAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3 +ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWm +p6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEA +AwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSEx +BhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElK +U1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3 +uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDwhYfa +plh9qsrD7VMsPtUqRFOqVVh9qmWH2qysPtUyw+1UpHbTqlZYfaplh9qsrD7VMsPtVqR206pVWGpl +h9qsrDUyw+1UpHbTqlZYalWH2q0sNSrD7VSkdtOqVlh9qmWH2qysPtUyw+1UpHdTqnHrD7VMsPtV +lYfaplh9q81SPyqnVKqw+1TLD7VZWH2qZYfaqUjtp1SssPtUyw+1WVh9qmWH2qlI7adUqrDUyw+1 +WVhqZYfarUjtp1SssNSrD7VaWGpVh9qpSO2nVKyw+1TLD7VZWH2qZYfaqUjup1Tj1h9qmWH2qysP +tUyw+1eapH5VTqlVYfaplh9qsrD7VMsPtVKR206pWWH2qZYfarKw+1TLD7VSkdtOqVVhqZYfarKw +1MsPtVqR206pWWGpVh9qtLDUqw+1UpHbTqlZYfaplh9qsrD7VMsPtVKR206px6w+1TLD7VZWH2qZ +YfavNUj8rp1SqsPtUyw+1WVh9qmWH2qlI7adUrLD7VMsPtVlYfaplh9qpSO2nVKqw1MsPtVlYamW +H2q1I7adUrLDUqw+1WlhqVYfaqUjtp1SssPtUyw+1WVh9qmWH2qlI7adU//Z + diff --git a/corpus/media-edit/cold/read/page-002.xml b/corpus/media-edit/cold/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..06fd70d877cf4fc736042f3721d922604bbf5548 --- /dev/null +++ b/corpus/media-edit/cold/read/page-002.xml @@ -0,0 +1,6 @@ + +iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec +CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI +E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp +oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT ++wHXVRp4qDc0HQAAAABJRU5ErkJggg== diff --git a/corpus/media-edit/cold/read/page-003.xml b/corpus/media-edit/cold/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..fe4862b04644ab0360f7d559524f5d9e58ede5f2 --- /dev/null +++ b/corpus/media-edit/cold/read/page-003.xml @@ -0,0 +1,6 @@ + +iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec +CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI +E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp +oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT ++wHXVRp4qDc0HQAAAABJRU5ErkJggg== diff --git a/corpus/media-edit/cold/read/page-004.xml b/corpus/media-edit/cold/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..4639561ea941e9e423acbc5fe06ad1644698fc3c --- /dev/null +++ b/corpus/media-edit/cold/read/page-004.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/media-edit/cold/read/page-005.xml b/corpus/media-edit/cold/read/page-005.xml new file mode 100644 index 0000000000000000000000000000000000000000..48734a443612be25fc28531731ea7c0aca3d68c2 --- /dev/null +++ b/corpus/media-edit/cold/read/page-005.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/media-edit/cold/read/page-006.xml b/corpus/media-edit/cold/read/page-006.xml new file mode 100644 index 0000000000000000000000000000000000000000..08773c95a324e440f88aeb89c56698693cc948ee --- /dev/null +++ b/corpus/media-edit/cold/read/page-006.xml @@ -0,0 +1,9 @@ + +iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAO +xAAADsQBlSsOGwAAARlJREFUeF7t2VFHQ2EcBvD3PWetVqvVam1ERESMiIiIiL7/J5p9gD1dvF51 +8eNcn4uf53n2P1ZLKUOpnkMCtYx0kkAt00F8gkAtsxFQBJpPAEWgxRGgCLScAopAq2NAEWhzAigC +3c0ARaD7U0AR6OEMUAR6nAOKQE/ngCLQ8wWgCLRdAIpAL5eAItDrFaAI9LYEFIHerwFFoI8bQBHo +cwUoAn3dAopA32tAEehnAygKjPu/xjyHBAY0WQDQL+0BBKhtYSVIgiSo6w+xiqmYiqnYX34J2SAb +ZINskA1qa4EESZAEdW1B15e7g9xBbf2VIAmSICPtDmprgQRJkAR1bUHXl7uD3EFt/ZUgCZIgI+0O +amuBBEnQP07QDo1OW207QPoWAAAAAElFTkSuQmCC + diff --git a/corpus/media-edit/cold/read/page-007.xml b/corpus/media-edit/cold/read/page-007.xml new file mode 100644 index 0000000000000000000000000000000000000000..e101f77116ccc48f07c4f8993e9f51c78626bb03 --- /dev/null +++ b/corpus/media-edit/cold/read/page-007.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/media-edit/cold/read/page-008.xml b/corpus/media-edit/cold/read/page-008.xml new file mode 100644 index 0000000000000000000000000000000000000000..022a3cda8d3d69c1b6746b6597d482872e5a3348 --- /dev/null +++ b/corpus/media-edit/cold/read/page-008.xml @@ -0,0 +1,6 @@ + +iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec +CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI +E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp +oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT ++wHXVRp4qDc0HQAAAABJRU5ErkJggg== diff --git a/corpus/media-edit/cold/read/page-009.xml b/corpus/media-edit/cold/read/page-009.xml new file mode 100644 index 0000000000000000000000000000000000000000..6b36a5c00b4b852f1b0efc8bf3e42a05c80ca966 --- /dev/null +++ b/corpus/media-edit/cold/read/page-009.xml @@ -0,0 +1,4 @@ + +Inherited strike and spacing]]>مرحبا بالعالم]]> diff --git a/corpus/media-edit/cold/read/page-010.xml b/corpus/media-edit/cold/read/page-010.xml new file mode 100644 index 0000000000000000000000000000000000000000..924580a23aa6902ea18ed48b5ab76ec3332a920c --- /dev/null +++ b/corpus/media-edit/cold/read/page-010.xml @@ -0,0 +1,46 @@ + +iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAMAAADS6oI8AAADAFBMVEW4t5mrt5O4qpWrqo6euo2S +uoeZs4ieqoiSqoK4nJCrnIq4kIyrkIaen4SSn36ZmH+ekH+SkHmGuoGGs395t3pounJos3CGqnx5 +qnVtqm9iqmqGnHd5nHGGkHN5kG1on2lomGdokGS4hIirhIK4d4Srd32eh3ySh3aZgHeed3eSd3G4 +a4Cra3m4YHyrYHaea3OSa22eYG+SYGmGh3CGgG55hGloh2FogF+Gd2t5d2Rtd15id1mGa2d5a2CG +YGN5YF1ta1pia1VoYFRWumlWs2dJt2I4ulo4s1hWqmRJql09qlcyqlImulEaukshs0wKukMKs0Em +qkwaqkYPqkAEqjtWnF9JnFlWkFtJkFU4n1E4mE84kEwmn0gan0IhmEMmkEMakD0KnzoKmDgKkDVW +h1hWgFZJhFE4h0k4gEdWd1NJd0w9d0Yyd0FWa09Ja0hWYEtJYEU9a0Iyaz04YDwmh0AahzohgDsK +hzIKgDAmdzsadzUPdy8EdyomazcaazEmYDMaYC0PaysEayYKYCW4VHirVHK4R3SrR22eV2ySV2aZ +UGeeR2eSR2G4O3CrO2m4MGyrMGaeO2OSO12eMF+SMFmGV2CGUF55VFloV1FoUE+GR1t5R1RtR05i +R0mGO1d5O1CGMFN5ME1tO0piO0VoMES4JGirJGK4GGSrGF6eJ1ySJ1aZIFeeGFeSGFG4DmGrDlq4 +A12rA1eeDlSSDk6eA1CSA0qGJE95JEmGGEt5GEVoJ0FoID9oGDyGDkh5DkGGA0R5Az5tDjtiDjZo +AzVWVEdJVEE4Vzk4UDdWR0NJRzw9RzYyRzFWOz9JOzhWMDtJMDU9OzIyOy04MCwmVzAaVyohUCsK +VyIKUCAmRysaRyUPRx8ERxomOycaOyEmMCMaMB0POxsEOxYKMBVWJDdJJDFWGDNJGC04Jyk4ICc4 +GCRWDjBJDilWAyxJAyY9DiMyDh44Ax0mJyAaJxohIBsmGBsaGBUKJxIKIBAKGA0mDhgaDhImAxQa +Aw4PDgwEDgcKAwZYdbPqAAAAAWJLR0QAiAUdSAAAAAxjbVBQSkNtcDA3MTIAAAADSABzvAAABjJJ +REFUWEedkImbTgUcha9ChZl2RUKR0TKTtGiRsZUSEpI0tKm0GoW0WSOjMmNpL82oFG1apJJltNDC +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 +AElFTkSuQmCC + diff --git a/corpus/media-edit/cold/read/page-011.xml b/corpus/media-edit/cold/read/page-011.xml new file mode 100644 index 0000000000000000000000000000000000000000..6c6b43a8da6ff70f118a4fc131e824286792fc9f --- /dev/null +++ b/corpus/media-edit/cold/read/page-011.xml @@ -0,0 +1,6 @@ + +iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec +CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI +E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp +oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT ++wHXVRp4qDc0HQAAAABJRU5ErkJggg== diff --git a/corpus/media-edit/cold/read/page-012.xml b/corpus/media-edit/cold/read/page-012.xml new file mode 100644 index 0000000000000000000000000000000000000000..39c09dd905736b20a2d03f7e178f04ed6e0a8248 --- /dev/null +++ b/corpus/media-edit/cold/read/page-012.xml @@ -0,0 +1,6 @@ + +iVBORw0KGgoAAAANSUhEUgAAAGAAAABACAIAAABqVuVZAAAAwUlEQVR4nO3ZwQ2DQBDF0Hkk/dec +CuASwXCwtBVY/mYBM3OMznEC4TsfMx1nEALk2o8ACZB/GpJBAiSD3PcgbmICJIM0MVvX/RokQDJI +E1ODvPOrS5EWIBmkialB1ntcpAVIBmli1rvTRVGAZJAmZj1AvawKkAzSxNQg6zEu0gIkgzQx69Hp +oihAMkgT85LTn1UBkkGamBpkvcdFWoBkkCZmvTtdFAVIBmli1gPUy6oAySBNTA2yHuMiLUAyyPMT ++wHXVRp4qDc0HQAAAABJRU5ErkJggg== diff --git a/corpus/media-edit/cold/read/payloads.json b/corpus/media-edit/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..a209cd162964418686ed4b959c89c4ce107cda5f --- /dev/null +++ b/corpus/media-edit/cold/read/payloads.json @@ -0,0 +1,18 @@ +[ + { + "sha256": "4cff0039dbdf53a59f5f4e4592c29a5297ea3b7acce24415c0ae83c36c6bd7c2", + "name": "Fictitious attachment.txt", + "object": "{4DE24944-9177-0A86-2CA3-43EAAD47BA51}{40}{B0}", + "kind": "InsertedFile", + "page": "{57F870C0-4994-044B-3FBF-D15BD6A07C26}{1}{B0}", + "bytes": 27 + }, + { + "sha256": "56d4af65701c26df20bd4021eda95b6e830348ce3a746086079fe89285548dc9", + "name": "silence.wav", + "object": "{4DE24944-9177-0A86-2CA3-43EAAD47BA51}{44}{B0}", + "kind": "MediaFile", + "page": "{57F870C0-4994-044B-3FBF-D15BD6A07C26}{1}{B0}", + "bytes": 16044 + } +] \ No newline at end of file diff --git a/corpus/media-edit/cold/run.json b/corpus/media-edit/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..f22e4a895345aed5ec75ac33a1f210a321c986c4 --- /dev/null +++ b/corpus/media-edit/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/media", + "expected_pages": 13, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/media-edit/cold/scripts/cold.ps1 b/corpus/media-edit/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/media-edit/cold/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/media-edit/cold/scripts/read.ps1 b/corpus/media-edit/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/media-edit/cold/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/media-edit/cold/source.json b/corpus/media-edit/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..ae7e9f3ebbef2b7bbacd3b9a301771964d4c3643 --- /dev/null +++ b/corpus/media-edit/cold/source.json @@ -0,0 +1,20 @@ +[ + { + "path": "Features.one", + "bytes": 123088, + "sha256": "a070197bb9c1c2f9fbbe39680173407bb9b5fead4bb442d03a84b77a4036d67b", + "mtime_ns": 1789288953404668177 + }, + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "7dfd85afb7d1e4cf389bf09b9c934bf4a8bc7922ee676d47c4878309322e72e6", + "mtime_ns": 1789288953405483509 + }, + { + "path": "expected-count.txt", + "bytes": 2, + "sha256": "3fdba35f04dc8c462986c992bcf875546257113072a909c162f7e470e581e278", + "mtime_ns": 1789288953413592244 + } +] diff --git a/corpus/media-edit/cold/teardown.json b/corpus/media-edit/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/media-edit/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/crates/canvas/src/document.rs b/crates/canvas/src/document.rs index 15d5c8f7be59bb006fe3261051f693c9a36b5284..c39ed873578593710123916b57b9be5c7f981884 100644 --- a/crates/canvas/src/document.rs +++ b/crates/canvas/src/document.rs @@ -42,6 +42,7 @@ pub(crate) fn node(text: Paragraph, format: Format) -> Result ExGuid { content: ParagraphContent::Text(content), lists: Vec::new(), tags: Vec::new(), + media: Default::default(), collapsed: false, }], unsupported: Vec::new(), diff --git a/crates/notebook/tests/sync_pages.rs b/crates/notebook/tests/sync_pages.rs index baffaa9274d7f4ea701e5d7ad69f131fdd12456d..14b91ecaa59634fae77dea998e68fe230d2b96aa 100644 --- a/crates/notebook/tests/sync_pages.rs +++ b/crates/notebook/tests/sync_pages.rs @@ -93,6 +93,7 @@ fn body_outline(page: &mut Page, text: &str) -> ExGuid { content: ParagraphContent::Text(content), lists: Vec::new(), tags: Vec::new(), + media: Default::default(), collapsed: false, }], unsupported: Vec::new(), diff --git a/crates/onestore/README.md b/crates/onestore/README.md index da3bb0f4fe9c8c4926085cb917bb6654478355ef..249ded674288292f417d54906bac30ebd5a23b0c 100644 --- a/crates/onestore/README.md +++ b/crates/onestore/README.md @@ -45,6 +45,7 @@ harness also accepts `--client-profile release`. | `Document::active`, `Document::pages_in`, `Revision::parents`, `RevisionIndex::active` | Resolve the active revision, the pages of a space and parent links without repeating the lookups | | `Page::copy` | The page's content under fresh identities for copying into another section, definitions and payloads included; indent levels only an outline group carries refuse | | `page::link::internal_link`, `page::link::parse_internal_link` | Build and read the `onenote:#…` URLs OneNote stores for links to sections, pages and paragraphs, by identity | +| `page::Recording`, `page::MediaIndex` | A recording attachment's identity and type, and a paragraph's link to a moment in recordings; read from native pages and kept through edits, never authored | | `page::Page`, `page::Paragraph`, `page::Ink`, `page::Math` | Build an editable page model (title, outlines, paragraphs with coalesced text spans, tables, images, attachments, ink drawings and handwriting decoded to stroke polylines in page points) with stored identities; equations parse from their linear text and run data into a tree that renders the MathML OneNote exports; content outside the model is retained as `Unsupported` | | `protected::UnlockedSection` (optional feature) | Own decoded buffers for explicit known-password inspection; clear those buffers on drop; derived document strings/exports remain caller-owned | | `create_section` | Create one page containing one plain-text paragraph and an author, including Unicode | diff --git a/crates/onestore/src/page/mod.rs b/crates/onestore/src/page/mod.rs index 95bf245bd0f3cb5f25cffbdd6ada12e2be6091b7..d23fb27b43bddf33145ab0e3ffcfb0c81e26223e 100644 --- a/crates/onestore/src/page/mod.rs +++ b/crates/onestore/src/page/mod.rs @@ -146,6 +146,7 @@ pub struct PageParagraph { pub content: ParagraphContent, pub lists: Vec, pub tags: Vec, + pub media: MediaIndex, pub collapsed: bool, } @@ -176,6 +177,25 @@ pub struct Attachment { pub bytes: Option>, #[serde(with = "payload")] pub preview: Option>, + /// Set when the file is an audio or video recording OneNote captured; annotations on + /// the page refer to it by identity (`PageParagraph::media`). + pub recording: Option, +} + +/// A recording's identity and OneNote's type code for it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Recording { + pub id: [u8; 16], + pub kind: u32, +} + +/// A paragraph's link to a moment in recordings on the page: OneNote plays from +/// `time_ms` when the paragraph is chosen. Read from native pages and preserved through +/// edits; OneNote alone creates them while recording. +#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct MediaIndex { + pub recordings: Vec<[u8; 16]>, + pub time_ms: Option, } impl PartialEq for Attachment { @@ -184,6 +204,7 @@ impl PartialEq for Attachment { && self.filename == other.filename && self.source_path == other.source_path && self.size == other.size + && self.recording == other.recording } } @@ -199,9 +220,10 @@ impl Attachment { preview, filename, source_path, + recording_id, + recording_type, icon_width, icon_height, - .. } = &node.kind else { unreachable!() @@ -226,6 +248,10 @@ impl Attachment { size: icon_width.zip(*icon_height).map(|(w, h)| [w, h]), bytes: payload(container)?, preview: payload(preview)?, + recording: recording_id.map(|id| Recording { + id, + kind: recording_type.unwrap_or(0), + }), }) } } @@ -699,6 +725,10 @@ fn read_paragraphs( .nodes .get(content_id) .ok_or_else(|| invalid("Missing canvas paragraph content"))?; + let media = MediaIndex { + recordings: content.media_ids.clone(), + time_ms: content.media_time_ms, + }; let content = if let Kind::RichText { paragraph_style, .. } = &content.kind @@ -844,6 +874,7 @@ fn read_paragraphs( content, lists: lists.clone(), tags: node.tags.clone(), + media, collapsed: *collapse_state == Some(1), }); pending.extend( diff --git a/crates/onestore/src/page/write.rs b/crates/onestore/src/page/write.rs index e6c8b8f1cea0bce230bf2c41621566de611ddb78..7fa8eecf5328521938fff47b9ff021d274dafff9 100644 --- a/crates/onestore/src/page/write.rs +++ b/crates/onestore/src/page/write.rs @@ -611,6 +611,9 @@ impl Lowering<'_> { let Some(previous) = old.paragraphs.get(id) else { continue; }; + if paragraph.media != previous.media { + return Err(invalid("Recording annotations cannot be edited")); + } let same = paragraph.style == previous.style && paragraph.format == previous.format; if !same { return Err(invalid( @@ -971,6 +974,9 @@ impl Lowering<'_> { } continue; } + if attachment.recording.is_some() { + return Err(invalid("Recordings are captured by OneNote, not inserted")); + } let Some(bytes) = &attachment.bytes else { return Err(invalid("A new attachment needs its payload")); }; @@ -1383,6 +1389,9 @@ impl Lowering<'_> { .to_le_bytes() .to_vec(), )]; + if attachment.recording != stored.recording { + return Err(invalid("A recording stays the recording OneNote captured")); + } let mut removed = Vec::new(); if attachment.filename != stored.filename { let name = crate::create::string(&attachment.filename); @@ -1490,6 +1499,11 @@ impl Lowering<'_> { "New paragraphs in a stored outline contain plain text without styles", )); } + if paragraph.media != Default::default() { + return Err(invalid( + "Recording annotations are made by OneNote while it records", + )); + } // A new table starts as an empty text paragraph whose content the // structure pass replaces with the table. let (text, text_id) = match ¶graph.content { diff --git a/crates/onestore/tests/page_attachments.rs b/crates/onestore/tests/page_attachments.rs index 89e0d1f3cfdb1bb6394075688ce6e9233df656e4..7304fb6d0355f13b8917f4df9f886235f2e48968 100644 --- a/crates/onestore/tests/page_attachments.rs +++ b/crates/onestore/tests/page_attachments.rs @@ -113,6 +113,7 @@ fn insert_attachment(section: &str, preview: Option>) -> Vec { size: Some([24.0, 24.0]), bytes: Some(Arc::from(PAYLOAD)), preview: preview.clone(), + recording: None, }); body_paragraphs(&mut after).push(holder.clone()); body_paragraphs(&mut after).push(plain_paragraph(&template, "After the file")); @@ -182,6 +183,7 @@ fn attachments_need_a_file_name_and_stored_ones_are_renamed_in_place() { size: None, bytes: Some(Arc::from(PAYLOAD)), preview: None, + recording: None, }); body_paragraphs(&mut after).push(holder); assert!(PreparedEdit::page(&source, space, &after, AUTHOR).is_err()); diff --git a/crates/onestore/tests/page_media.rs b/crates/onestore/tests/page_media.rs new file mode 100644 index 0000000000000000000000000000000000000000..9f24c297c880a8041b58fd91eb6d2b9f604f13d9 --- /dev/null +++ b/crates/onestore/tests/page_media.rs @@ -0,0 +1,147 @@ +use onestore::{ + ExGuid, PreparedEdit, RevisionIndex, Store, + document::Document, + page::{MediaIndex, Page, PageObject, ParagraphContent, Recording}, +}; + +/// A page OneNote 2010 recorded audio on (`corpus/m6/native-features-01`): a recording +/// attachment and a paragraph annotated with a moment in it. +const NATIVE: &[u8] = include_bytes!("../../../corpus/m6/native-features-01/notebook/Features.one"); +const NATIVE_READ: &str = include_str!("../../../corpus/m6/native-features-01/read/page-017.xml"); +const TITLE: &str = "Files and recording"; + +fn page(bytes: &[u8]) -> (ExGuid, Page) { + let store = Store::parse(bytes).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + document + .pages() + .unwrap() + .into_iter() + .map(|(space, _)| (space, Page::from_space(&document, space).unwrap())) + .find(|(_, page)| page.title == TITLE) + .unwrap() +} + +fn media_id(read: &str) -> [u8; 16] { + let start = read.find("mediaID=\"").unwrap() + 9; + let guid = &read[start..read[start..].find('"').unwrap() + start]; + format!("{guid},0").parse::().unwrap().guid +} + +fn recording(page: &Page) -> (Recording, MediaIndex) { + let mut recording = None; + let mut annotation = None; + for object in &page.objects { + let PageObject::Outline(outline) = object else { + continue; + }; + for paragraph in &outline.paragraphs { + if let ParagraphContent::Attachment(attachment) = ¶graph.content + && let Some(found) = attachment.recording + { + recording = Some(found); + } + if !paragraph.media.recordings.is_empty() { + annotation = Some(paragraph.media.clone()); + } + } + } + (recording.unwrap(), annotation.unwrap()) +} + +#[test] +fn a_native_recording_and_its_annotation_read_as_the_export_names_them() { + let (_, native) = page(NATIVE); + let (recording, annotation) = recording(&native); + let id = media_id(NATIVE_READ); + assert_eq!(recording.id, id); + assert_eq!( + annotation, + MediaIndex { + recordings: vec![id], + time_ms: Some(500) + } + ); + assert!(NATIVE_READ.contains("timeIndex=\"500\"")); +} + +/// `ONESTORE_MEDIA_EXPORT` names a new directory receiving the edited section for a cold +/// reopen: the recording and its annotation survive an ordinary edit of the page. +#[test] +fn editing_a_recorded_page_keeps_the_recording_and_its_annotation() { + use onestore::page::{TextObject, text::new_id}; + let (space, before) = page(NATIVE); + let mut after = before.clone(); + let outline = after + .objects + .iter_mut() + .find_map(|object| match object { + PageObject::Outline(outline) if !outline.title => Some(outline), + _ => None, + }) + .unwrap(); + let mut added = outline.paragraphs[0].clone(); + added.id = new_id().unwrap(); + added.parent = None; + added.level = 1; + added.style = None; + added.lists.clear(); + added.tags.clear(); + added.media = MediaIndex::default(); + added.content = ParagraphContent::Text(TextObject { + id: new_id().unwrap(), + date_field: None, + text: onestore::page::Paragraph::new("Added after recording".into(), Default::default()), + tags: Vec::new(), + }); + outline.paragraphs.push(added.clone()); + let written = PreparedEdit::page(NATIVE, space, &after, "Media author").unwrap(); + let (_, stored) = page(written.as_bytes()); + assert_eq!(recording(&stored), recording(&before)); + assert_eq!(stored.objects.len(), before.objects.len()); + + let mut annotated = after.clone(); + let outline = annotated + .objects + .iter_mut() + .find_map(|object| match object { + PageObject::Outline(outline) if !outline.title => Some(outline), + _ => None, + }) + .unwrap(); + let last = outline.paragraphs.last_mut().unwrap(); + last.media = recording(&before).1; + assert!(PreparedEdit::page(written.as_bytes(), space, &annotated, "x").is_err()); + let mut recaptured = after.clone(); + for object in &mut recaptured.objects { + if let PageObject::Outline(outline) = object { + for paragraph in &mut outline.paragraphs { + if let ParagraphContent::Attachment(attachment) = &mut paragraph.content { + attachment.recording = None; + } + } + } + } + assert!(PreparedEdit::page(written.as_bytes(), space, &recaptured, "x").is_err()); + + if let Some(directory) = std::env::var_os("ONESTORE_MEDIA_EXPORT") { + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir_all(&directory).unwrap(); + std::fs::write(directory.join("Features.one"), written.as_bytes()).unwrap(); + let file_id = Store::parse(written.as_bytes()).unwrap().header.file_id; + std::fs::write( + directory.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents( + "Open Notebook.onetoc2", + &[("Features.one", file_id)], + ) + .unwrap(), + ) + .unwrap(); + let store = Store::parse(written.as_bytes()).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let count = Document::parse(&index).unwrap().pages().unwrap().len(); + std::fs::write(directory.join("expected-count.txt"), count.to_string()).unwrap(); + } +} diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index aeb18075979bba53c2030be9bfa8db908cc91fd4..61d283e44b1c19f25dceae3d451a4848803b8cce 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -652,6 +652,7 @@ dependencies = [ "nix", "roxmltree", "serde", + "serde_json", "sha1", "subtle", "zeroize", diff --git a/tools/test_media_edit.py b/tools/test_media_edit.py new file mode 100644 index 0000000000000000000000000000000000000000..46cded49f156bd13f0ef46d8d37ede03f73c4ca4 --- /dev/null +++ b/tools/test_media_edit.py @@ -0,0 +1,34 @@ +from pathlib import Path +import runpy +import shutil +from tempfile import TemporaryDirectory +import unittest +import xml.etree.ElementTree as ET + +from native_xml import ns + +ROOT = Path(__file__).resolve().parent.parent +FIXTURE = ROOT / 'corpus/media-edit' +compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] + + +class MediaEditTest(unittest.TestCase): + def test_an_edit_next_to_a_recording_keeps_the_recording_and_its_annotation(self): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(FIXTURE / 'cold/read', read) + compare(FIXTURE / 'candidate', read) + pages = [ET.parse(path).getroot() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml'))] + page = next(page for page in pages if page.get('name') == 'Files and recording') + media = page.find('.//one:MediaFile', ns) + self.assertEqual(media.get('preferredName'), 'silence.wav') + recording = media.find('one:MediaReference', ns).get('mediaID') + index = page.find('.//one:MediaIndex', ns) + self.assertEqual(index.get('timeIndex'), '500') + self.assertEqual(index.find('one:MediaReference', ns).get('mediaID'), recording) + texts = [t.text for t in page.findall('.//one:T', ns) if t.text] + self.assertIn('Added after recording', texts) + + +if __name__ == '__main__': + unittest.main()