commit f581cbdcedf5ed625dbe2485e42d5190bca95900 Author: Albrecht Degering Date: Wed Jul 22 17:06:03 2026 +0200 feat: add local OneNote and ONEPKG reader diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f648236 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +coverage/ +release/ +*.tsbuildinfo +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..90e6041 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@add-ideas:registry=https://git.add-ideas.de/api/packages/zemion/npm/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f6277ce --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +coverage +dist +release +package-lock.json +LICENSES/*.txt diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..94e30b6 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "es5", + "printWidth": 80 +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2a4a56e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +## 0.1.0 - 2026-07-22 + +- Add a standalone React/Vite reader integrated with Toolbox SDK 0.1.1, + including `AppShell`, the Toolbox manifest, nested-path-safe assets, and the + Portal release contract. +- Add a worker-hosted native TypeScript parser for a desktop OneNote + revision-store subset: bounded file-node and object-space traversal, + property sets, page metadata, titles, timestamps, levels, and plain text. +- Add in-memory CAB enumeration and extraction for uncompressed and LZX + folders, including checksums, strict paths, explicit resource limits, and + cooperative cancellation between data blocks. +- Open contained `.one` sections independently and expose package entries, + section/page navigation, partial failures, and structured diagnostics. +- Verify a Joplin `.one` golden with three pages and a Joplin LZX `0x1203` + `.onepkg` golden with seven entries, five sections, and seven pages. Also + verify rust-cab uncompressed and LZX `0x1303` fixtures. +- Add deterministic static release packaging, SHA-256 sidecars, pinned-source + provenance, fixture licensing, and security/support documentation. + +Known unsupported areas are FSSHTTP-packaged `.one`, MSZIP and Quantum CAB +extraction, multi-cabinet archives, `.onetoc2` hierarchy/order interpretation, +visual content, attachments, layout/format fidelity, links, and exports. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bbe986b --- /dev/null +++ b/LICENSE @@ -0,0 +1,4 @@ +SPDX-License-Identifier: MPL-2.0 + +OneNote Tools is licensed under the Mozilla Public License, version 2.0. +The complete, unmodified licence text is provided in LICENSES/MPL-2.0.txt. diff --git a/LICENSES/AGPL-3.0.txt b/LICENSES/AGPL-3.0.txt new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSES/AGPL-3.0.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSES/MPL-2.0.txt b/LICENSES/MPL-2.0.txt new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/LICENSES/MPL-2.0.txt @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/LICENSES/lzxd-MIT.txt b/LICENSES/lzxd-MIT.txt new file mode 100644 index 0000000..7d19ec8 --- /dev/null +++ b/LICENSES/lzxd-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Lonami + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSES/rust-cab-MIT.txt b/LICENSES/rust-cab-MIT.txt new file mode 100644 index 0000000..ae6bf8d --- /dev/null +++ b/LICENSES/rust-cab-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Matthew D. Steele + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0839089 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# OneNote Tools + +OneNote Tools is a standalone, local-first browser reader for a tested subset +of Microsoft OneNote `.one` sections and CAB-based `.onepkg` packages. The +parser is native TypeScript, runs in a dedicated module worker, and never +uploads or persists the selected notebook. + +It is also an independent application in the add·ideas Toolbox contract: the +source uses the Toolbox SDK shell and manifest, builds with Vite `base: './'`, +and is delivered to Toolbox Portal as an already-built, checksummed static ZIP. + +## What version 0.1.0 can open + +- Desktop revision-store `.one` sections from the tested OneNote family. The + reader exposes section/page names, stable page IDs, created and updated + timestamps when present, page level, and readable plain text. +- Single-cabinet `.onepkg` files whose CAB folders are uncompressed or LZX + compressed. Each contained desktop `.one` section is parsed independently, + so one unsupported section does not hide sections that did parse. +- Package entries, safe normalized paths, uncompressed sizes, compression + metadata, section/page navigation, and structured diagnostics. + +The golden tests parse a real Joplin desktop `.one` file with three pages and a +real Joplin `.onepkg` with seven entries, five sections, and seven pages. LZX +`0x1203` and `0x1303` data and an uncompressed CAB are covered by fixtures. + +This is a structural reader, not a full-fidelity OneNote renderer. Images, +tables, ink, embedded files, layout, formatting, links, and exports are not +rendered. FSSHTTP-packaged `.one`, MSZIP and Quantum CAB extraction, +multi-cabinet archives, and `.onetoc2` hierarchy/order interpretation are not +supported. See [format support](docs/FORMAT_SUPPORT.md) for the precise matrix. + +There is no Rust/Wasm runtime, server, proxy, upload, telemetry, or notebook +storage. All parsing and CAB/LZX decompression are ordinary browser JavaScript +produced from the TypeScript source. + +## Use + +Run the static build or open it through Toolbox Portal, then drop or choose one +local `.one` or `.onepkg` file. Use **Clear file** to terminate the current +worker and release application references. Browser garbage collection does not +provide a secure-erasure guarantee. + +## Development + +Node.js 22 or newer is required. The checked-in `.npmrc` contains only the +public package registry mapping and no credential. + +```sh +npm ci +npm run dev +npm run check +``` + +`npm run check` validates manifest synchronization, TypeScript, ESLint, +Prettier, fixture-backed Vitest tests, the production build, and +`toolbox-check dist`. + +The application works standalone. A same-origin Toolbox catalogue can be +supplied through the SDK's `?toolbox=` context mechanism. The app uses the SDK +`AppShell` and does not add a second suite-level switcher. + +## Release artifact + +```sh +npm run release:artifact +(cd release && sha256sum -c onenote-tools-0.1.0.sha256) +``` + +The ZIP root contains the built static app and manifest plus the changelog, +corresponding-source information, security/support/provenance documents, and +the required project and third-party licence texts. File order and ZIP +timestamps are fixed. Existing artifacts are not overwritten unless `--force` +is passed after `--`. + +Toolbox Portal expands the reviewed ZIP below an `apps//` path and +locks its exact SHA-256. It does not build this repository or import this app +into the portal React tree. + +## Design and provenance + +- [Format support](docs/FORMAT_SUPPORT.md) +- [Security model and active limits](docs/SECURITY.md) +- [Pinned references and normative specifications](docs/REFERENCE_IMPLEMENTATIONS.md) +- [Port provenance](docs/PORTING.md) +- [Fixture provenance](tests/fixtures/README.md) + +## Licence + +OneNote Tools is licensed under MPL-2.0. The complete licence is in +[`LICENSES/MPL-2.0.txt`](LICENSES/MPL-2.0.txt). Adapted and third-party material +remains under the terms recorded in +[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md). diff --git a/SOURCE.md b/SOURCE.md new file mode 100644 index 0000000..3f7e7a4 --- /dev/null +++ b/SOURCE.md @@ -0,0 +1,17 @@ +# Corresponding source + +The corresponding source for OneNote Tools 0.1.0 is the `v0.1.0` tag: + +https://git.add-ideas.de/zemion/onenote-tools/src/tag/v0.1.0 + +Build the release from that tag with Node.js 22 or newer: + +```sh +npm ci +npm run release:artifact +``` + +The committed lockfile fixes the npm dependency graph. Exact reference +implementation revisions and the source-to-TypeScript mapping are recorded in +`docs/REFERENCE_IMPLEMENTATIONS.md` and `docs/PORTING.md`. Third-party terms +are reproduced in `LICENSES/` and summarized in `THIRD_PARTY_NOTICES.md`. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..5e23d07 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,38 @@ +# Third-party notices + +The production application uses: + +- `@add-ideas/toolbox-contract` 0.1.1, Apache-2.0; +- `@add-ideas/toolbox-shell-react` 0.1.1, Apache-2.0; +- React, React DOM, and Scheduler, MIT. + +The release packager collects the complete installed runtime licence texts into +`LICENSES/THIRD-PARTY-LICENSES.txt`. + +The native TypeScript OneStore/MS-ONE parser follows selected structures and +traversal from `onenote.rs` (MPL-2.0), pinned at +`5138a39a3f4e72b840932f9872fecde52fa9da60`. Adapted source files carry MPL +provenance headers. + +The browser-native CAB parser/checksum implementation is a translated and +security-bounded adaptation of `rust-cab` (MIT), pinned at +`c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2`. The LZX decoder is a translated +adaptation of `lzxd` 0.2.5 (MIT OR Apache-2.0), pinned at +`4748e43594e3e30cff2ace3a6ad7a376c9816fdd`; this project uses it under the MIT +option. Adapted modules carry provenance headers and the licence texts are in +`LICENSES/rust-cab-MIT.txt` and `LICENSES/lzxd-MIT.txt`. + +The Base64-encoded `onenote_desktop.one` test fixture comes from Joplin at +commit `28eb53bd9f0cd4ae0dec0c59a08037075a38302b` and is distributed under +AGPL-3.0-or-later. Its licence text is in `LICENSES/AGPL-3.0.txt`. + +The Base64-encoded Joplin `test.onepkg` fixture is pinned at commit +`1e73aad7eb08fde5d9e4cb533df40052a5cd32d7` and is likewise test-only data +distributed under AGPL-3.0-or-later. + +The encoded fixture bytes are excluded from `dist` and the release ZIP. The +release retains the fixture provenance document and AGPL text so these notices +remain complete. + +The remaining npm packages are build and test tooling and are not shipped as +runtime modules in the static application. diff --git a/docs/FORMAT_SUPPORT.md b/docs/FORMAT_SUPPORT.md new file mode 100644 index 0000000..513365a --- /dev/null +++ b/docs/FORMAT_SUPPORT.md @@ -0,0 +1,57 @@ +# Format support + +## Version 0.1.0 support matrix + +| Input or feature | Status | Tested evidence and boundary | +| ---------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------- | +| Desktop revision-store `.one` | Supported subset | Real 34,904-byte Joplin golden; three pages | +| FSSHTTP-packaged `.one` | Unsupported | Header is detected and rejected with a controlled diagnostic | +| Unknown/legacy `.one` variants | Unsupported | File type and format GUIDs are validated before traversal | +| `.onepkg`, single CAB, LZX | Supported subset | Real Joplin package: LZX `0x1203`, three CFDATA blocks, seven entries, five sections, seven pages | +| CAB LZX `0x1303` | Supported extraction | Byte-for-byte rust-cab MIT unit fixture, two text entries | +| CAB without compression | Supported extraction | Byte-for-byte rust-cab MIT unit fixture | +| CAB MSZIP | Unsupported | Metadata is recognized; extraction fails with `cab-compression-unsupported` | +| CAB Quantum | Unsupported | Metadata is recognized; extraction fails with `cab-compression-unsupported` | +| Multi-cabinet CAB / spanning files | Unsupported | Header flags and spanning folder indexes are rejected | +| `.onetoc2` package entries | Listed only | Files are identified, but notebook hierarchy and section order are not interpreted | +| Standalone `.onetoc2` | Unsupported in the app | The picker accepts only `.one` and `.onepkg` | + +“Supported subset” is intentionally narrower than “all files with this +extension.” A successful open requires the tested desktop OneStore structures +and content graph nodes. Malformed offsets, counts, references, CAB checksums, +paths, compressed data, and unsupported variants produce bounded diagnostics or +a controlled failure. + +The default LZX limit accepts CAB window sizes through 8 MiB, but compatibility +claims are fixture-backed only for window bits 18 (`0x1203`) and 19 (`0x1303`). +Trailing CAB data is ignored with a warning. Reserved CAB areas are bounded and +skipped. CAB names are decoded as strict UTF-8; unmarked valid UTF-8 receives a +warning, while invalid non-ASCII legacy-code-page names are rejected rather +than guessed. + +## Extracted OneNote data + +The desktop section path currently returns: + +- section display name; +- page ID, title, created/updated timestamp when available, and page level; +- visible Unicode/extended-ASCII rich text as plain text and a text preview; +- diagnostics tied to an offset, structure, or page where available. + +It does not preserve visual position, rich-text formatting, list formatting, +tables, images, ink, equations, embedded files, audio/video, active links, +page versions/conflicts, or exports. Unsupported content objects remain visible +as diagnostics instead of being silently presented as parsed. + +Within a supported `.onepkg`, each `.one` entry is parsed independently. +Malformed or unsupported sections are marked failed/unsupported while other +sections remain available. Package navigation is temporary normalized-path +order until `.onetoc2` interpretation is implemented. + +## Test fixtures + +The exact hashes, source revisions, licences, expected page names, and CAB +structures are recorded in +[`tests/fixtures/README.md`](../tests/fixtures/README.md). Tests use no private +notebooks and do not treat a synthetic archive as proof of Microsoft OneNote +export compatibility. diff --git a/docs/PORTING.md b/docs/PORTING.md new file mode 100644 index 0000000..c75f66f --- /dev/null +++ b/docs/PORTING.md @@ -0,0 +1,33 @@ +# Port provenance + +The desktop `.one` reader adapts selected structures from the pinned +`onenote.rs` implementation. It is not a line-for-line or complete port. + +| TypeScript module | Upstream path(s) | Revision | Port scope and deviations | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `src/onenote/onestore/file-node.ts` | `onestore/desktop/file_structure/file_node_list_fragment.rs`, `file_node/*`, chunk references | `onenote.rs@5138a39a3f4e72b840932f9872fecde52fa9da60` | Selected; immutable byte input, strict range checks, fragment/child cycle detection, and depth/count limits | +| `src/onenote/onestore/property-set.ts` | `onestore/shared/{object_prop_set,prop_set,property,object_stream_header}.rs` | same | Substantial; bounded vectors/counts/nesting and serializable discriminated unions | +| `src/onenote/onestore/desktop-store.ts` | `onestore/desktop/{file_node,file_structure,objects}/*` | same | Selected desktop section path; explicit missing-reference errors; file-data/attachment payloads skipped with diagnostics | +| `src/onenote/one/{constants,section}.ts` | `one/property*`, `one/property_set/*`, `onenote/{section,page,page_series,outline,rich_text}.rs` | same | Page metadata, titles, and plain text only; bounded graph traversal; visual/embedded content diagnosed | + +The browser-native package path has separate MIT provenance: + +| TypeScript module | Upstream path(s) | Revision and selected licence | Port scope and deviations | +| -------------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/onenote/onepkg/{cabinet,checksum}.ts` | rust-cab `src/{cabinet,folder,file,ctype,checksum}.rs` | `c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2`, MIT | CAB reading/checksum adapted for immutable browser bytes, strict normalized paths, checked ranges/checksums, whole-folder extraction, explicit limits, and cancellation/yield hooks; writers and MSZIP are not ported | +| `src/onenote/onepkg/lzx/{bit-reader,huffman,decoder}.ts` | lzxd 0.2.5 `src/{bitstream,tree,block,window,lib}.rs` | `4748e43594e3e30cff2ace3a6ad7a376c9816fdd`, MIT option of MIT OR Apache-2.0 | Directly translated core with typed arrays, CAB CFDATA boundaries, persistent folder state, strict malformed-stream errors, and operation/window/cancellation limits; delta reference data is omitted | + +The bounded binary readers, format detection, parser limits/error model, path +policy, worker protocol/DTOs, package-to-section boundary, and application UI +are original TypeScript implementations guided by the normative specifications +and project requirements. + +Files adapted from `onenote.rs` carry an MPL-2.0 notice, SPDX identifier, +upstream path/revision, and deviations. Files adapted from rust-cab or lzxd +carry their MIT provenance, copyright notice, exact revision, and deviations. +Licence texts are reproduced in `LICENSES/` and included in the release ZIP. + +Focused tests cover successful pinned fixtures plus malformed bounds, +references, checksums, paths, Huffman trees, compressed blocks, limits, and +cancellation. Fixture origins and redistribution terms are recorded separately +in `tests/fixtures/README.md`. diff --git a/docs/REFERENCE_IMPLEMENTATIONS.md b/docs/REFERENCE_IMPLEMENTATIONS.md new file mode 100644 index 0000000..a5094da --- /dev/null +++ b/docs/REFERENCE_IMPLEMENTATIONS.md @@ -0,0 +1,46 @@ +# Specifications and pinned references + +Version 0.1.0 records these immutable implementation revisions: + +| Repository | Exact revision | Use | +| ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------- | +| [`toolbox-sdk`](https://git.add-ideas.de/zemion/toolbox-sdk/src/commit/5e206baf42dfae5a00f2743fb2cc501bb12d9b7a) | `5e206baf42dfae5a00f2743fb2cc501bb12d9b7a` | App contract, React shell, manifest schema/testkit, and nested-path behavior | +| [`toolbox-portal`](https://git.add-ideas.de/zemion/toolbox-portal/src/commit/5d2e466ad61e36a0e2be2a2d8459b1a127f53f11) | `5d2e466ad61e36a0e2be2a2d8459b1a127f53f11` | Reviewed ZIP shape, exact checksum lock, and independent-app assembly model | +| [`onenote.rs`](https://github.com/msiemens/onenote.rs/tree/5138a39a3f4e72b840932f9872fecde52fa9da60) | `5138a39a3f4e72b840932f9872fecde52fa9da60` | Selected desktop OneStore and MS-ONE structures adapted into TypeScript | +| [`rust-cab`](https://github.com/mdsteele/rust-cab/tree/c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2) | `c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2` | CAB structures, checksum, enumeration/extraction, and two unit fixtures | +| [`lzxd` 0.2.5](https://github.com/Lonami/lzxd/tree/4748e43594e3e30cff2ace3a6ad7a376c9816fdd) | `4748e43594e3e30cff2ace3a6ad7a376c9816fdd` | LZX bitstream, Huffman, block/window, match, and E8 logic adapted into TypeScript | + +The SDK and Portal revisions were inspected from the local repositories. The +three parser references were cloned and reviewed at the hashes above; a branch +name or package version alone is not used as provenance. Source-level mappings +and behavioural deviations are in [PORTING.md](PORTING.md). + +## Normative Microsoft documents + +The implementation uses the official specifications as the source of format +truth and the pinned projects as implementation references: + +- [[MS-ONESTORE]: OneNote Revision Store File Format](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-onestore/ae670cd2-4b38-4b24-82d1-87cfb2cc3725), + particularly sections 2.2.4 (chunk references), 2.3.1 (header), 2.4 + (file-node lists and fragments), 2.5.1-2.5.8 (object-space/revision + manifests), and 2.6.7 (property sets). +- [[MS-ONE]: OneNote File Format](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-one/73d22548-a613-4350-8c23-07d15576be50), + particularly sections 2.1.8-2.1.13 (root objects, page object spaces, and + property sets), 2.2.17-2.2.23 (section/page-series/outline/rich-text nodes), + and 2.2.30, 2.2.34, 2.2.62-2.2.65, and 2.2.91 (page metadata/manifest, + titles, page-space references, and section name). +- [[MS-CAB]: Cabinet File Format](https://download.microsoft.com/download/4/d/a/4da14f27-b4ef-4170-a6e6-5b1ef85b1baa/%5Bms-cab%5D.pdf), + sections 2.1-2.4 (`CFHEADER`, `CFFOLDER`, `CFFILE`, and `CFDATA`) and 3.1 + (checksum method). +- [[MS-PATCH]: LZX Delta Compression and Decompression](https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-patch/cc78752a-b4af-4eee-88cb-01f4d8a4c2bf), + sections 2.1-2.7 (bitstream/window concepts, header/E8 translation, blocks, + Huffman trees, pretree encoding, tokens, and decoding). `lzxd` 0.2.5 states + that it was based on revision 9.0; the port removes delta/reference-data use + and applies CAB CFDATA chunk framing. + +The Microsoft documents are linked rather than copied. Their published pages +can change independently; the source references and golden bytes remain pinned +for reproducibility. + +`one2html` is deliberately not pinned or inspected because visual HTML +rendering is outside this release. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..141ba76 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,113 @@ +# Security model + +## Trust boundary and local processing + +Selected `.one` and `.onepkg` files are untrusted. The main thread checks the +source size, reads one file into an `ArrayBuffer`, transfers it to a dedicated +module worker, and receives serializable data-transfer objects. CAB entries are +extracted only into memory; archive paths are never written to the filesystem. + +The worker is a fault-containment and responsiveness boundary, not a security +sandbox. A successful open keeps its worker session alive for page requests. +Clearing the file, choosing another file, or unmounting the app terminates that +worker; a failed open also terminates it. JavaScript garbage collection does +not promise immediate release or secure erasure. + +The application does not: + +- upload or transmit notebook bytes; +- use a backend, proxy, telemetry, analytics, remote fonts, or external content + fetching; +- persist source or derived content in LocalStorage, IndexedDB, Cache Storage, + cookies, or a service worker; +- execute notebook content or inject notebook HTML/SVG; +- use WebAssembly, a Rust runtime, or a native helper. + +## Active default limits + +The main thread rejects a source larger than 512 MiB before reading it. The +desktop section parser then applies these independent defaults: + +| Resource | Default limit | +| ------------------------------------- | -------------------: | +| Section bytes | 512 MiB | +| File-node list fragments / nesting | 16,384 / 64 | +| File nodes | 1,000,000 | +| Objects | 500,000 | +| Properties per set / property nesting | 16,384 / 64 | +| Stream IDs | 1,000,000 | +| One property vector | 64 MiB | +| Object references | 1,000,000 | +| Content-graph depth | 256 | +| Pages per section | 100,000 | +| Diagnostics per section | 10,000 | +| Extracted text per section | 2,000,000 characters | + +The CAB/package path applies these defaults: + +| Resource | Default limit | +| ------------------------------- | -------------: | +| Cabinet input | 512 MiB | +| Folders / files | 1,024 / 10,000 | +| CFDATA blocks | 131,072 | +| CAB name bytes | 255 | +| Path characters / path segments | 4,096 / 64 | +| One extracted file | 256 MiB | +| One folder output | 512 MiB | +| Total folder output | 512 MiB | +| Total extracted entry bytes | 512 MiB | +| Folder compression ratio | 200:1 | +| LZX window | 8 MiB | +| LZX decode operations | 1,000,000,000 | + +A CFDATA block may declare at most 32 KiB of output. Counts and offsets use +checked arithmetic and bounded readers. The CAB parser verifies supplied block +checksums, rejects overlapping/out-of-range data, rejects absolute/traversal, +empty, control-character, and case-insensitive duplicate paths, and refuses +multi-cabinet/spanning entries. + +LZX checks its window, Huffman trees, block sizes, match ranges, output size, +and operation budget. CAB extraction checks cancellation and yields between +data blocks so the worker can process termination. Desktop OneStore traversal +checks fragment/reference cycles, nesting, object/property/reference counts, +and byte ranges. + +## Rendering and links + +DTO strings are rendered as React text nodes. Notebook HTML, active SVG, +scripts, attachment payloads, and automatic external navigation are not +rendered. Link activation is not implemented. If links are added, they require +explicit user action, scheme validation, and safe navigation attributes. + +## Known gaps and roadmap + +- There is no wall-clock deadline. Synchronous desktop `.one` parsing cannot + observe cooperative cancellation until control returns; terminating the + worker is the current hard stop. +- The package flow holds the source, decompressed folder buffers, extracted + entry slices, and parsed DTOs in memory at overlapping times. The byte/count + limits bound these individually but do not guarantee a low peak heap. +- Section text/page/diagnostic limits apply per contained section rather than + once to the complete package. +- CAB checksums detect accidental corruption but do not authenticate a package. +- Format coverage is narrow and has only the documented golden files. New + producer/version claims require licensed regression fixtures and malformed + variants. +- The worker boundary reduces UI impact but is not a substitute for browser and + operating-system isolation. Open especially sensitive files in a dedicated + browser profile if that risk matters. + +Planned hardening includes an elapsed-time budget, cancellation checkpoints in +desktop OneStore graph traversal, aggregate package DTO/text budgets, lower +peak-memory extraction, expanded malformed-input/fuzz corpora, and additional +licensed OneNote export fixtures. + +## Fixtures and vulnerability reporting + +Never commit private notebooks. Fixture provenance and redistribution terms +must be recorded in `tests/fixtures/README.md`. + +Report suspected parser vulnerabilities privately to the repository owner +through the Gitea instance before publishing malicious fixtures. Include the +application version, browser, smallest reproducible input, diagnostic, and +observed resource use. Do not attach sensitive notebooks. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..2212508 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,34 @@ +import js from '@eslint/js'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist', 'release', 'coverage', 'node_modules'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2022, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, + { + files: ['scripts/**/*.mjs'], + languageOptions: { globals: globals.node }, + }, + eslintConfigPrettier +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..e52dc77 --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + + OneNote Tools + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4e40d13 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4007 @@ +{ + "name": "onenote-tools", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "onenote-tools", + "version": "0.1.0", + "license": "MPL-2.0", + "dependencies": { + "@add-ideas/toolbox-contract": "0.1.1", + "@add-ideas/toolbox-shell-react": "0.1.1", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@add-ideas/toolbox-testkit": "0.1.1", + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.8.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^10.4.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "fflate": "^0.8.3", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "prettier": "^3.8.3", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3", + "vite": "^8.0.13", + "vitest": "^4.1.6" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@add-ideas/toolbox-contract": { + "version": "0.1.1", + "resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-contract/-/0.1.1/toolbox-contract-0.1.1.tgz", + "integrity": "sha512-0q2whA5Y4SqG4PNNleXYR8nGbT3UYsZ1x+6ZWVmxuc/WjMxmujhD7ilIISmFzmnNdbyEyv/Zpr+U0fnHqGODqw==", + "license": "Apache-2.0" + }, + "node_modules/@add-ideas/toolbox-shell-react": { + "version": "0.1.1", + "resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.1.1/toolbox-shell-react-0.1.1.tgz", + "integrity": "sha512-Di2mu1BoJrWJg4/v5jDExROXDpvLXorlUgqcsBQV8ea/LklKoBFTAfgTkk/23HZ2BzixyMIjrhl9kD507s+iPQ==", + "license": "Apache-2.0", + "dependencies": { + "@add-ideas/toolbox-contract": "0.1.1" + }, + "peerDependencies": { + "react": ">=18 <20", + "react-dom": ">=18 <20" + } + }, + "node_modules/@add-ideas/toolbox-testkit": { + "version": "0.1.1", + "resolved": "https://git.add-ideas.de/api/packages/zemion/npm/%40add-ideas%2Ftoolbox-testkit/-/0.1.1/toolbox-testkit-0.1.1.tgz", + "integrity": "sha512-o+H7LfM7ipdgmYz18S0lLuN6JsXUR+8ubAbcjTWTk8rSqNfp1JyeoRHOPbJVEHXrQwIFe/OB86UzRpex2gnKUQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@add-ideas/toolbox-contract": "0.1.1" + }, + "bin": { + "toolbox-check": "dist/cli.js" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", + "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.0.tgz", + "integrity": "sha512-oCu2wfipvX3AePSgmOuKkIywOu+8n9psz7hXYmk56ghpu3+7KzNIBopaOs4c9BrtdnTtW30unG9GTfHo7EwERQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.395", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz", + "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", + "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.22", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz", + "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.9.tgz", + "integrity": "sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.9" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.9.tgz", + "integrity": "sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..22ea2f8 --- /dev/null +++ b/package.json @@ -0,0 +1,58 @@ +{ + "name": "onenote-tools", + "version": "0.1.0", + "description": "A local-first browser workspace for native OneNote parsing.", + "license": "MPL-2.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "predev": "npm run manifest:generate", + "dev": "vite", + "prebuild": "npm run manifest:generate", + "build": "tsc -b && vite build", + "preview": "vite preview", + "typecheck": "tsc -b --pretty false", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "vitest run", + "test:watch": "vitest", + "manifest:generate": "node scripts/generate-toolbox-manifest.mjs", + "manifest:check": "node scripts/generate-toolbox-manifest.mjs --check", + "toolbox:check": "toolbox-check dist", + "release:artifact": "npm run check && node scripts/package-release.mjs", + "check": "npm run manifest:check && npm run typecheck && npm run lint && npm run format:check && npm run test && npm run build && npm run toolbox:check" + }, + "dependencies": { + "@add-ideas/toolbox-contract": "0.1.1", + "@add-ideas/toolbox-shell-react": "0.1.1", + "react": "^19.2.6", + "react-dom": "^19.2.6" + }, + "devDependencies": { + "@add-ideas/toolbox-testkit": "0.1.1", + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/node": "^25.8.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^10.4.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "fflate": "^0.8.3", + "globals": "^17.6.0", + "jsdom": "^29.1.1", + "prettier": "^3.8.3", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.3", + "vite": "^8.0.13", + "vitest": "^4.1.6" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..ad41ef1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/toolbox-app.json b/public/toolbox-app.json new file mode 100644 index 0000000..b839de2 --- /dev/null +++ b/public/toolbox-app.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json", + "schemaVersion": 1, + "id": "de.add-ideas.onenote-tools", + "name": "OneNote Tools", + "description": "Local-first reader for tested desktop revision-store .one sections and LZX .onepkg packages; other OneNote formats are not implemented.", + "entry": "./", + "icon": "./favicon.svg", + "categories": ["documents", "onenote"], + "tags": ["local", "read-only", "one", "onepkg", "cab", "lzx"], + "integration": { + "contextVersion": 1, + "launchModes": ["navigate", "new-tab"], + "embedding": "unsupported" + }, + "requirements": { + "secureContext": false, + "workers": true, + "indexedDb": false, + "crossOriginIsolated": false + }, + "privacy": { + "processing": "local", + "fileUploads": false, + "telemetry": false, + "label": "Local only · no uploads · no storage · no telemetry" + }, + "source": { + "repository": "https://git.add-ideas.de/zemion/onenote-tools", + "license": "MPL-2.0" + }, + "version": "0.1.0" +} diff --git a/scripts/generate-toolbox-manifest.mjs b/scripts/generate-toolbox-manifest.mjs new file mode 100644 index 0000000..1e99664 --- /dev/null +++ b/scripts/generate-toolbox-manifest.mjs @@ -0,0 +1,64 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { parseToolboxApp } from '@add-ideas/toolbox-contract'; +import { format } from 'prettier'; + +const projectRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..' +); +const packageJsonPath = path.join(projectRoot, 'package.json'); +const definitionPath = path.join( + projectRoot, + 'src', + 'toolbox', + 'manifest.definition.json' +); +const versionSourcePath = path.join(projectRoot, 'src', 'version.ts'); +const manifestPath = path.join(projectRoot, 'public', 'toolbox-app.json'); + +const [packageJson, definition, versionSource] = await Promise.all([ + readJson(packageJsonPath), + readJson(definitionPath), + readFile(versionSourcePath, 'utf8'), +]); + +const sourceVersion = versionSource.match( + /APP_VERSION\s*=\s*['"]([^'"]+)['"]/ +)?.[1]; + +if (!sourceVersion) { + throw new Error('Could not read APP_VERSION from src/version.ts.'); +} +if (sourceVersion !== packageJson.version) { + throw new Error( + `Version mismatch: package.json is ${packageJson.version}, while src/version.ts is ${sourceVersion}.` + ); +} + +const manifest = { + $schema: + 'https://git.add-ideas.de/zemion/toolbox-sdk/raw/branch/main/schemas/toolbox-app.v1.schema.json', + schemaVersion: 1, + ...definition, + version: packageJson.version, +}; +parseToolboxApp(manifest); +const output = await format(JSON.stringify(manifest), { parser: 'json' }); + +if (process.argv.includes('--check')) { + const existing = await readFile(manifestPath, 'utf8').catch(() => ''); + if (existing !== output) { + throw new Error( + 'public/toolbox-app.json is out of date. Run npm run manifest:generate.' + ); + } +} else { + await writeFile(manifestPath, output); +} + +async function readJson(filePath) { + return JSON.parse(await readFile(filePath, 'utf8')); +} diff --git a/scripts/package-release.mjs b/scripts/package-release.mjs new file mode 100644 index 0000000..d1bf2e0 --- /dev/null +++ b/scripts/package-release.mjs @@ -0,0 +1,126 @@ +import { createHash } from 'node:crypto'; +import { access, mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + collectDirectoryFiles, + createDeterministicZip, + createThirdPartyLicenseBundle, +} from './release-archive.mjs'; + +const modulePath = fileURLToPath(import.meta.url); +const defaultProjectRoot = path.resolve(path.dirname(modulePath), '..'); + +export async function collectReleaseMetadataFiles(projectRoot) { + const files = {}; + + for (const fileName of [ + 'CHANGELOG.md', + 'LICENSE', + 'README.md', + 'SOURCE.md', + 'THIRD_PARTY_NOTICES.md', + ]) { + files[fileName] = await readFile(path.join(projectRoot, fileName)); + } + + for (const licenseName of [ + 'AGPL-3.0.txt', + 'MPL-2.0.txt', + 'lzxd-MIT.txt', + 'rust-cab-MIT.txt', + ]) { + files[`LICENSES/${licenseName}`] = await readFile( + path.join(projectRoot, 'LICENSES', licenseName) + ); + } + + files['LICENSES/THIRD-PARTY-LICENSES.txt'] = + await createThirdPartyLicenseBundle(projectRoot, [ + '@add-ideas/toolbox-contract', + '@add-ideas/toolbox-shell-react', + 'react', + 'react-dom', + 'scheduler', + ]); + + for (const documentName of [ + 'FORMAT_SUPPORT.md', + 'PORTING.md', + 'REFERENCE_IMPLEMENTATIONS.md', + 'SECURITY.md', + ]) { + files[`docs/${documentName}`] = await readFile( + path.join(projectRoot, 'docs', documentName) + ); + } + + files['tests/fixtures/README.md'] = await readFile( + path.join(projectRoot, 'tests', 'fixtures', 'README.md') + ); + + return files; +} + +async function packageRelease(projectRoot, arguments_) { + const packageJson = JSON.parse( + await readFile(path.join(projectRoot, 'package.json'), 'utf8') + ); + const releaseDirectory = path.join(projectRoot, 'release'); + const archiveName = `onenote-tools-${packageJson.version}.zip`; + const archivePath = path.join(releaseDirectory, archiveName); + const checksumPath = path.join( + releaseDirectory, + `onenote-tools-${packageJson.version}.sha256` + ); + const force = arguments_.includes('--force'); + const unknownArguments = arguments_.filter( + (argument) => argument !== '--force' + ); + if (unknownArguments.length > 0) { + throw new Error(`Unknown release argument: ${unknownArguments.join(', ')}`); + } + + await mkdir(releaseDirectory, { recursive: true }); + if (!force) { + await assertDoesNotExist(archivePath, projectRoot); + await assertDoesNotExist(checksumPath, projectRoot); + } + + const files = await collectDirectoryFiles(path.join(projectRoot, 'dist')); + if (Object.keys(files).some((fileName) => fileName.endsWith('.wasm'))) { + throw new Error('Release input contains a forbidden WebAssembly file.'); + } + Object.assign(files, await collectReleaseMetadataFiles(projectRoot)); + + const archive = createDeterministicZip(files); + const checksum = createHash('sha256').update(archive).digest('hex'); + const writeFlag = force ? 'w' : 'wx'; + await writeFile(archivePath, archive, { flag: writeFlag }); + await writeFile(checksumPath, `${checksum} ${archiveName}\n`, { + encoding: 'utf8', + flag: writeFlag, + }); + + console.log(`Created release/${archiveName}`); + console.log(`SHA-256 ${checksum}`); +} + +async function assertDoesNotExist(outputPath, projectRoot) { + try { + await access(outputPath); + } catch (error) { + if (error && typeof error === 'object' && error.code === 'ENOENT') return; + throw error; + } + throw new Error( + `${path.relative(projectRoot, outputPath)} already exists; pass --force to replace this exact version.` + ); +} + +if ( + process.argv[1] !== undefined && + path.resolve(process.argv[1]) === modulePath +) { + await packageRelease(defaultProjectRoot, process.argv.slice(2)); +} diff --git a/scripts/release-archive.d.mts b/scripts/release-archive.d.mts new file mode 100644 index 0000000..7030c06 --- /dev/null +++ b/scripts/release-archive.d.mts @@ -0,0 +1,12 @@ +export function compareCodePoints(left: string, right: string): number; +export function createDeterministicZip( + files: Record +): Uint8Array; +export function collectDirectoryFiles( + directory: string, + prefix?: string +): Promise>; +export function createThirdPartyLicenseBundle( + projectRoot: string, + packageNames: string[] +): Promise; diff --git a/scripts/release-archive.mjs b/scripts/release-archive.mjs new file mode 100644 index 0000000..3ffcb29 --- /dev/null +++ b/scripts/release-archive.mjs @@ -0,0 +1,118 @@ +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; +import { zipSync } from 'fflate'; + +const LICENSE_FILE = /^(?:licen[cs]e|copying|notice)(?:[._-].*)?$/iu; + +export function compareCodePoints(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +export function createDeterministicZip(files) { + const orderedFiles = Object.fromEntries( + Object.entries(files).sort(([left], [right]) => + compareCodePoints(left, right) + ) + ); + return zipSync(orderedFiles, { + level: 9, + mtime: new Date(1980, 0, 1, 0, 0, 0), + }); +} + +export async function collectDirectoryFiles(directory, prefix = '') { + const result = {}; + const entries = await readdir(directory, { withFileTypes: true }); + entries.sort((left, right) => compareCodePoints(left.name, right.name)); + + for (const entry of entries) { + const absolutePath = path.join(directory, entry.name); + const relativePath = path.posix.join(prefix, entry.name); + if (entry.isDirectory()) { + Object.assign( + result, + await collectDirectoryFiles(absolutePath, relativePath) + ); + } else if (entry.isFile()) { + result[relativePath] = await readFile(absolutePath); + } else { + throw new Error( + `Unsupported filesystem entry in release input: ${relativePath}` + ); + } + } + return result; +} + +export async function createThirdPartyLicenseBundle(projectRoot, packageNames) { + const sections = []; + for (const packageName of [...new Set(packageNames)].sort( + compareCodePoints + )) { + const packageDirectory = path.join( + projectRoot, + 'node_modules', + ...packageName.split('/') + ); + const packageJson = JSON.parse( + await readFile(path.join(packageDirectory, 'package.json'), 'utf8') + ); + const licenseFiles = await collectLicenseFiles(packageDirectory); + if (licenseFiles.length === 0) { + throw new Error(`No license text found for ${packageName}.`); + } + + const contents = []; + for (const licenseFile of licenseFiles) { + const text = await readFile( + path.join(packageDirectory, ...licenseFile.split('/')), + 'utf8' + ); + contents.push( + `--- ${licenseFile} ---\n${text.replaceAll('\r\n', '\n').trim()}\n` + ); + } + sections.push( + [ + '================================================================================', + `${packageJson.name ?? packageName}@${packageJson.version ?? 'unknown'}`, + `Declared license: ${String(packageJson.license ?? 'unspecified')}`, + '================================================================================', + ...contents, + ].join('\n') + ); + } + + return Buffer.from( + [ + 'Runtime third-party software license texts bundled with OneNote Tools', + 'Generated deterministically from installed production packages.', + '', + ...sections, + '', + ].join('\n'), + 'utf8' + ); +} + +async function collectLicenseFiles(directory, prefix = '') { + const matches = []; + const entries = await readdir(directory, { withFileTypes: true }); + entries.sort((left, right) => compareCodePoints(left.name, right.name)); + + for (const entry of entries) { + if (entry.name === 'node_modules') continue; + const relativePath = path.posix.join(prefix, entry.name); + const absolutePath = path.join(directory, entry.name); + if (entry.isDirectory()) { + matches.push(...(await collectLicenseFiles(absolutePath, relativePath))); + } else if (entry.isFile() && LICENSE_FILE.test(entry.name)) { + matches.push(relativePath); + } else if (!entry.isFile()) { + throw new Error( + `Unsupported filesystem entry in license input: ${relativePath}` + ); + } + } + return matches; +} diff --git a/src/App.test.tsx b/src/App.test.tsx new file mode 100644 index 0000000..d689121 --- /dev/null +++ b/src/App.test.tsx @@ -0,0 +1,333 @@ +import { fireEvent, render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it } from 'vitest'; + +import { OneNoteApplication } from './App.js'; +import type { + OneNotePackageDto, + OneNoteSectionDto, +} from './onenote/model/dto.js'; +import { + MAX_SOURCE_FILE_BYTES, + sourceKindFromFileName, +} from './source-file.js'; +import { + OneNoteWorkerClient, + type WorkerPort, +} from './worker/onenote.client.js'; +import type { + WorkerRequest, + WorkerResponse, +} from './worker/worker-protocol.js'; + +const section: OneNoteSectionDto = { + format: 'one', + sourceName: 'Section.one', + sectionName: 'Project notes', + pages: [ + { + id: 'page-1', + title: 'First meeting', + text: 'This is the complete extracted page text.', + textPreview: 'This is the complete extracted page text.', + createdAt: '2024-01-02T10:00:00.000Z', + }, + ], + diagnostics: [ + { + severity: 'warning', + code: 'UNSUPPORTED_IMAGE', + message: 'An image was not rendered.', + recoverable: true, + }, + ], + parserInfo: { + implementation: 'typescript', + supportedVariant: 'desktop-revision-store', + referenceRevision: 'test', + }, +}; + +class SuccessfulParserWorker implements WorkerPort { + onmessage: ((event: MessageEvent) => void) | null = null; + onerror: ((event: ErrorEvent) => void) | null = null; + terminated = false; + + constructor(private readonly packageResult?: OneNotePackageDto) {} + + postMessage(message: WorkerRequest): void { + queueMicrotask(() => { + if (message.type === 'parse-one') { + this.respond({ + type: 'section', + requestId: message.requestId, + sessionId: 'session-one', + section, + }); + } else if (message.type === 'parse-onepkg' && this.packageResult) { + this.respond({ + type: 'package', + requestId: message.requestId, + sessionId: 'session-package', + notebook: this.packageResult, + }); + } else if (message.type === 'get-page') { + const page = section.pages[0]!; + this.respond({ + type: 'page', + requestId: message.requestId, + sessionId: message.sessionId, + page: { + ...page, + id: message.pageId, + blocks: [{ kind: 'text', text: page.text }], + diagnostics: [], + }, + }); + } + }); + } + + terminate(): void { + this.terminated = true; + } + + private respond(response: WorkerResponse): void { + this.onmessage?.(new MessageEvent('message', { data: response })); + } +} + +class FailingParserWorker implements WorkerPort { + onmessage: ((event: MessageEvent) => void) | null = null; + onerror: ((event: ErrorEvent) => void) | null = null; + + postMessage(message: WorkerRequest): void { + if (message.type !== 'parse-one' && message.type !== 'parse-onepkg') return; + queueMicrotask(() => { + this.onmessage?.( + new MessageEvent('message', { + data: { + type: 'failure', + requestId: message.requestId, + error: { + code: 'invalid-format', + message: 'The source header is not a recognized OneNote file.', + recoverable: false, + structure: 'OneStore header', + }, + } satisfies WorkerResponse, + }) + ); + }); + } + + terminate(): void {} +} + +describe('OneNoteApplication', () => { + it('recognizes only supported local filename extensions', () => { + expect(sourceKindFromFileName('Section.ONE')).toBe('one'); + expect(sourceKindFromFileName('Notebook.OnePkg')).toBe('onepkg'); + expect(sourceKindFromFileName('notes.zip')).toBeNull(); + }); + + it('opens a section, navigates its page, shows diagnostics, and clears', async () => { + const user = userEvent.setup(); + const worker = new SuccessfulParserWorker(); + render( + new OneNoteWorkerClient(() => worker)} + /> + ); + + await user.upload( + screen.getByLabelText('Choose a OneNote file'), + new File([new Uint8Array([1, 2, 3])], 'Section.one') + ); + + expect( + await screen.findByRole('heading', { name: 'First meeting' }) + ).toBeInTheDocument(); + expect( + within(screen.getByRole('article', { name: 'First meeting' })).getByText( + 'This is the complete extracted page text.' + ) + ).toBeInTheDocument(); + expect(screen.getByText('UNSUPPORTED_IMAGE')).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'Clear file' })); + expect(worker.terminated).toBe(true); + expect( + screen.getByRole('heading', { name: 'Open a OneNote file' }) + ).toBeInTheDocument(); + }); + + it('opens a dropped OneNote package through the local worker', async () => { + const notebook: OneNotePackageDto = { + format: 'onepkg', + sourceName: 'Notebook.onepkg', + sourceSize: 1, + entries: [], + sections: [], + diagnostics: [], + }; + render( + + new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook)) + } + /> + ); + + fireEvent.drop(screen.getByTestId('file-drop-zone'), { + dataTransfer: { + files: [new File([new Uint8Array([1])], 'Notebook.onepkg')], + }, + }); + + expect( + await screen.findByRole('heading', { name: 'Package summary' }) + ).toBeInTheDocument(); + }); + + it('keeps a failed packaged section visible beside a readable one', async () => { + const notebook: OneNotePackageDto = { + format: 'onepkg', + sourceName: 'Notebook.onepkg', + sourceSize: 4096, + entries: [ + { + path: 'Notes/Readable.one', + normalizedPath: 'Notes/Readable.one', + kind: 'section', + uncompressedSize: 2048, + compressionMethod: 'LZX', + parseStatus: 'parsed', + }, + { + path: 'Notes/Broken.one', + normalizedPath: 'Notes/Broken.one', + kind: 'section', + uncompressedSize: 1024, + compressionMethod: 'LZX', + parseStatus: 'failed', + }, + ], + sections: [ + { + id: 'readable', + path: 'Notes/Readable.one', + displayName: 'Readable', + parseStatus: 'parsed', + section, + diagnostics: [], + }, + { + id: 'broken', + path: 'Notes/Broken.one', + displayName: 'Broken', + parseStatus: 'failed', + diagnostics: [ + { + severity: 'error', + code: 'SECTION_PARSE_FAILED', + message: 'This section is malformed.', + recoverable: true, + }, + ], + }, + ], + diagnostics: [ + { + severity: 'warning', + code: 'TOC_NOT_INTERPRETED', + message: 'Package path order is temporary.', + recoverable: true, + }, + ], + }; + const user = userEvent.setup(); + render( + + new OneNoteWorkerClient(() => new SuccessfulParserWorker(notebook)) + } + /> + ); + + await user.upload( + screen.getByLabelText('Choose a OneNote file'), + new File([new Uint8Array([1])], 'Notebook.onepkg') + ); + + expect( + await screen.findByRole('heading', { name: 'Package summary' }) + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: /Broken, failed/i }) + ).toBeVisible(); + expect(screen.getByText('TOC_NOT_INTERPRETED')).toBeInTheDocument(); + expect( + await within( + screen.getByRole('article', { name: 'First meeting' }) + ).findByText('This is the complete extracted page text.') + ).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: /Broken, failed/i })); + expect(screen.getByText('SECTION_PARSE_FAILED')).toBeInTheDocument(); + expect( + screen.getByText('This section did not produce a readable page list.') + ).toBeInTheDocument(); + }); + + it('shows a controlled parser failure', async () => { + const user = userEvent.setup(); + render( + + new OneNoteWorkerClient(() => new FailingParserWorker()) + } + /> + ); + + await user.upload( + screen.getByLabelText('Choose a OneNote file'), + new File(['not OneNote'], 'Broken.one') + ); + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'The source header is not a recognized OneNote file.' + ); + expect(screen.getByRole('alert')).toHaveTextContent( + 'invalid-format · OneStore header' + ); + }); + + it('rejects unrelated extensions without starting a worker', async () => { + const user = userEvent.setup({ applyAccept: false }); + render(); + + await user.upload( + screen.getByLabelText('Choose a OneNote file'), + new File(['not a notebook'], 'notes.zip') + ); + + expect(await screen.findByRole('alert')).toHaveTextContent( + 'Choose a file whose name ends in .one or .onepkg.' + ); + }); + + it('enforces the source-size safety limit before reading', async () => { + const user = userEvent.setup(); + render(); + const file = new File(['tiny'], 'large.one'); + Object.defineProperty(file, 'size', { + configurable: true, + value: MAX_SOURCE_FILE_BYTES + 1, + }); + + await user.upload(screen.getByLabelText('Choose a OneNote file'), file); + + expect(await screen.findByRole('alert')).toHaveTextContent('512 MiB'); + }); +}); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..999b332 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,410 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { AppShell } from '@add-ideas/toolbox-shell-react'; +import '@add-ideas/toolbox-shell-react/styles.css'; + +import { DiagnosticsPanel } from './components/DiagnosticsPanel.js'; +import { FileDropZone } from './components/FileDropZone.js'; +import { HelpDialog } from './components/HelpDialog.js'; +import { PackageInspector } from './components/PackageInspector.js'; +import { PageReader } from './components/PageReader.js'; +import { + PackageNavigation, + SingleSectionNavigation, +} from './components/SectionNavigation.js'; +import type { + OneNotePackageDto, + OneNotePageDto, + OneNoteSectionDto, +} from './onenote/model/dto.js'; +import type { ParserDiagnostic } from './onenote/model/diagnostics.js'; +import { + MAX_SOURCE_FILE_BYTES, + sourceKindFromFileName, +} from './source-file.js'; +import { toolboxApp } from './toolbox/manifest.js'; +import { + OneNoteWorkerClient, + WorkerClientError, +} from './worker/onenote.client.js'; + +type LoadedSource = + | { + kind: 'one'; + sessionId: string; + section: OneNoteSectionDto; + } + | { + kind: 'onepkg'; + sessionId: string; + notebook: OneNotePackageDto; + }; + +type ApplicationState = + | { phase: 'idle' } + | { phase: 'reading'; fileName: string } + | { phase: 'loaded'; fileName: string; source: LoadedSource } + | { + phase: 'error'; + title: string; + message: string; + detail?: string; + }; + +type PageState = + | { phase: 'empty' } + | { phase: 'loading'; pageId: string } + | { phase: 'loaded'; page: OneNotePageDto } + | { phase: 'error'; message: string }; + +function uniqueDiagnostics( + diagnostics: ParserDiagnostic[] +): ParserDiagnostic[] { + const seen = new Set(); + return diagnostics.filter((diagnostic) => { + const key = JSON.stringify([ + diagnostic.severity, + diagnostic.code, + diagnostic.message, + diagnostic.offset, + diagnostic.structure, + diagnostic.propertyId, + ]); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); +} + +export interface OneNoteApplicationProps { + createWorkerClient?: () => OneNoteWorkerClient; +} + +export function OneNoteApplication({ + createWorkerClient = () => new OneNoteWorkerClient(), +}: OneNoteApplicationProps) { + const [state, setState] = useState({ phase: 'idle' }); + const [pageState, setPageState] = useState({ phase: 'empty' }); + const [selectedSectionId, setSelectedSectionId] = useState< + string | undefined + >(); + const activeClient = useRef(null); + const generation = useRef(0); + const pageGeneration = useRef(0); + + const clearFile = useCallback(() => { + generation.current += 1; + pageGeneration.current += 1; + activeClient.current?.terminate(); + activeClient.current = null; + setSelectedSectionId(undefined); + setPageState({ phase: 'empty' }); + setState({ phase: 'idle' }); + }, []); + + useEffect(() => { + return () => { + generation.current += 1; + pageGeneration.current += 1; + activeClient.current?.terminate(); + }; + }, []); + + const loadPage = async ( + sessionId: string, + pageId: string, + sectionId?: string + ) => { + const client = activeClient.current; + const requestGeneration = ++pageGeneration.current; + if (!client) { + setPageState({ + phase: 'error', + message: 'The local parser session is no longer available.', + }); + return; + } + + setPageState({ phase: 'loading', pageId }); + try { + const response = await client.getPage(sessionId, pageId, sectionId); + if (requestGeneration !== pageGeneration.current) return; + if (response.type === 'failure') { + setPageState({ phase: 'error', message: response.error.message }); + return; + } + setPageState({ phase: 'loaded', page: response.page }); + } catch (error) { + if (requestGeneration !== pageGeneration.current) return; + setPageState({ + phase: 'error', + message: + error instanceof WorkerClientError + ? error.message + : 'The selected page could not be read from the parser session.', + }); + } + }; + + const openFile = async (file: File) => { + const sourceKind = sourceKindFromFileName(file.name); + if (!sourceKind) { + setState({ + phase: 'error', + title: 'Unsupported file name', + message: 'Choose a file whose name ends in .one or .onepkg.', + }); + return; + } + if (file.size > MAX_SOURCE_FILE_BYTES) { + setState({ + phase: 'error', + title: 'File is too large', + message: 'The selected file exceeds the current 512 MiB safety limit.', + }); + return; + } + + generation.current += 1; + pageGeneration.current += 1; + const requestGeneration = generation.current; + activeClient.current?.terminate(); + const client = createWorkerClient(); + activeClient.current = client; + setSelectedSectionId(undefined); + setPageState({ phase: 'empty' }); + setState({ phase: 'reading', fileName: file.name }); + + try { + const bytes = await file.arrayBuffer(); + if (generation.current !== requestGeneration) return; + const response = await client.parseSource(sourceKind, file.name, bytes); + if (generation.current !== requestGeneration) return; + + if (response.type === 'failure') { + client.terminate(); + if (activeClient.current === client) activeClient.current = null; + setState({ + phase: 'error', + title: 'Could not open this OneNote source', + message: response.error.message, + detail: response.error.structure + ? `${response.error.code} · ${response.error.structure}` + : response.error.code, + }); + return; + } + + if (response.type === 'section') { + setState({ + phase: 'loaded', + fileName: file.name, + source: { + kind: 'one', + sessionId: response.sessionId, + section: response.section, + }, + }); + const firstPage = response.section.pages[0]; + if (firstPage) void loadPage(response.sessionId, firstPage.id); + return; + } + + const preferredSection = + response.notebook.sections.find( + (section) => + section.parseStatus === 'parsed' && + section.section !== undefined && + section.section.pages.length > 0 + ) ?? + response.notebook.sections.find( + (section) => section.parseStatus === 'parsed' && section.section + ) ?? + response.notebook.sections[0]; + setSelectedSectionId(preferredSection?.id); + setState({ + phase: 'loaded', + fileName: file.name, + source: { + kind: 'onepkg', + sessionId: response.sessionId, + notebook: response.notebook, + }, + }); + const firstPage = preferredSection?.section?.pages[0]; + if (firstPage) { + void loadPage(response.sessionId, firstPage.id, preferredSection?.id); + } + } catch (error) { + if (generation.current !== requestGeneration) return; + client.terminate(); + if (activeClient.current === client) activeClient.current = null; + setState({ + phase: 'error', + title: 'Local parser stopped', + message: + error instanceof WorkerClientError + ? error.message + : 'The local file could not be handed to the parser worker.', + }); + } + }; + + const selectedPageId = + pageState.phase === 'loading' + ? pageState.pageId + : pageState.phase === 'loaded' + ? pageState.page.id + : undefined; + const selectedPage = + pageState.phase === 'loaded' ? pageState.page : undefined; + + const selectPackageSection = (sectionId: string) => { + if (state.phase !== 'loaded' || state.source.kind !== 'onepkg') return; + setSelectedSectionId(sectionId); + const section = state.source.notebook.sections.find( + (candidate) => candidate.id === sectionId + ); + const firstPage = section?.section?.pages[0]; + if (firstPage) { + void loadPage(state.source.sessionId, firstPage.id, sectionId); + } else { + pageGeneration.current += 1; + setPageState({ phase: 'empty' }); + } + }; + + const visibleDiagnostics = uniqueDiagnostics( + (() => { + if (state.phase !== 'loaded') return []; + const pageDiagnostics = selectedPage?.diagnostics ?? []; + if (state.source.kind === 'one') { + return [...state.source.section.diagnostics, ...pageDiagnostics]; + } + const selectedSection = state.source.notebook.sections.find( + (section) => section.id === selectedSectionId + ); + return [ + ...state.source.notebook.diagnostics, + ...(selectedSection?.diagnostics ?? []), + ...(selectedSection?.section?.diagnostics ?? []), + ...pageDiagnostics, + ]; + })() + ); + + return ( +
+ {state.phase === 'idle' ? ( + void openFile(file)} /> + ) : null} + + {state.phase === 'reading' ? ( +
+

Local parser worker

+

Opening {state.fileName}

+

+ Validating the source and extracting supported content in this + browser. +

+ +
+ ) : null} + + {state.phase === 'loaded' ? ( + <> +
+
+

+ {state.source.kind === 'one' ? 'OneNote section' : 'ONEPKG'} +

+

{state.fileName}

+
+ +
+ + {state.source.kind === 'onepkg' ? ( + + ) : null} + +
+ {state.source.kind === 'one' ? ( + + void loadPage(state.source.sessionId, pageId) + } + /> + ) : ( + + void loadPage( + state.source.sessionId, + pageId, + selectedSectionId + ) + } + /> + )} + +
+ + + + ) : null} + + {state.phase === 'error' ? ( +
+

Could not open file

+

{state.title}

+

{state.message}

+ {state.detail ? {state.detail} : null} +
+ +
+
+ ) : null} + + +
+ ); +} + +export function App() { + const [helpOpen, setHelpOpen] = useState(false); + + return ( + setHelpOpen(true)}> + Help + + } + > + + {helpOpen ? setHelpOpen(false)} /> : null} + + ); +} diff --git a/src/components/DiagnosticsPanel.tsx b/src/components/DiagnosticsPanel.tsx new file mode 100644 index 0000000..d2cb665 --- /dev/null +++ b/src/components/DiagnosticsPanel.tsx @@ -0,0 +1,70 @@ +import type { ParserDiagnostic } from '../onenote/model/diagnostics.js'; + +interface DiagnosticsPanelProps { + diagnostics: ParserDiagnostic[]; + title?: string; +} + +function formatOffset(offset: number): string { + return `0x${offset.toString(16).toUpperCase()}`; +} + +export function DiagnosticsPanel({ + diagnostics, + title = 'Diagnostics', +}: DiagnosticsPanelProps) { + return ( +
+
+
+

Parser details

+

{title}

+
+ {diagnostics.length} +
+ + {diagnostics.length === 0 ? ( +

No parser diagnostics for this view.

+ ) : ( +
    + {diagnostics.map((diagnostic, index) => ( +
  • +
    + {diagnostic.code} + {diagnostic.severity} +
    +

    {diagnostic.message}

    + {diagnostic.structure !== undefined || + diagnostic.offset !== undefined || + diagnostic.propertyId !== undefined ? ( +
    + {diagnostic.structure !== undefined ? ( + <> +
    Structure
    +
    {diagnostic.structure}
    + + ) : null} + {diagnostic.offset !== undefined ? ( + <> +
    Offset
    +
    {formatOffset(diagnostic.offset)}
    + + ) : null} + {diagnostic.propertyId !== undefined ? ( + <> +
    Property
    +
    {diagnostic.propertyId}
    + + ) : null} +
    + ) : null} +
  • + ))} +
+ )} +
+ ); +} diff --git a/src/components/FileDropZone.tsx b/src/components/FileDropZone.tsx new file mode 100644 index 0000000..d51f020 --- /dev/null +++ b/src/components/FileDropZone.tsx @@ -0,0 +1,70 @@ +import { useRef, useState, type ChangeEvent, type DragEvent } from 'react'; + +interface FileDropZoneProps { + disabled?: boolean; + onFile(file: File): void; +} + +export function FileDropZone({ disabled = false, onFile }: FileDropZoneProps) { + const input = useRef(null); + const [dragging, setDragging] = useState(false); + + const selectFile = (event: ChangeEvent) => { + const file = event.target.files?.[0]; + event.target.value = ''; + if (file) onFile(file); + }; + + const dropFile = (event: DragEvent) => { + event.preventDefault(); + setDragging(false); + if (disabled) return; + const file = event.dataTransfer.files[0]; + if (file) onFile(file); + }; + + return ( +
{ + event.preventDefault(); + if (!disabled) setDragging(true); + }} + onDragOver={(event) => event.preventDefault()} + onDragLeave={(event) => { + if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { + setDragging(false); + } + }} + onDrop={dropFile} + data-testid="file-drop-zone" + > +

Private, local-first reader

+

Open a OneNote file

+

+ Inspect a .one section or .onepkg notebook + package without sending it to a server. +

+ + + + or drop one .one or .onepkg file here + +
+ ); +} diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx new file mode 100644 index 0000000..6ba5c65 --- /dev/null +++ b/src/components/HelpDialog.tsx @@ -0,0 +1,37 @@ +interface HelpDialogProps { + onClose(): void; +} + +export function HelpDialog({ onClose }: HelpDialogProps) { + return ( +
+
+

About OneNote Tools

+

+ Read the tested desktop revision-store .one subset and single-cabinet + LZX or uncompressed .onepkg packages with a native TypeScript parser. + Unsupported content remains visible as diagnostics instead of being + silently discarded. +

+

+ FSSHTTP sections, MSZIP and Quantum packages, multi-cabinet archives, + notebook TOC ordering, visual fidelity, attachments, and export are + not supported in this release. +

+

+ Nothing is uploaded, persisted, or sent to telemetry. Closing or + clearing releases application references and terminates the worker; + this is not a secure-erasure guarantee. +

+ +
+
+ ); +} diff --git a/src/components/PackageInspector.tsx b/src/components/PackageInspector.tsx new file mode 100644 index 0000000..6b51c1a --- /dev/null +++ b/src/components/PackageInspector.tsx @@ -0,0 +1,93 @@ +import type { OneNotePackageDto } from '../onenote/model/dto.js'; + +interface PackageInspectorProps { + notebook: OneNotePackageDto; +} + +function formatBytes(value: number): string { + if (value < 1024) return `${value} B`; + const units = ['KiB', 'MiB', 'GiB']; + let amount = value / 1024; + let unit = units[0]!; + for (let index = 1; index < units.length && amount >= 1024; index += 1) { + amount /= 1024; + unit = units[index]!; + } + return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`; +} + +export function PackageInspector({ notebook }: PackageInspectorProps) { + const totalExpanded = notebook.entries.reduce( + (total, entry) => total + entry.uncompressedSize, + 0 + ); + const sectionCount = notebook.entries.filter( + (entry) => entry.kind === 'section' + ).length; + + return ( +
+
+
+

CAB package

+

Package summary

+
+
+
+
+
Source size
+
{formatBytes(notebook.sourceSize)}
+
+
+
Entries
+
{notebook.entries.length}
+
+
+
Sections
+
{sectionCount}
+
+
+
Expanded size
+
{formatBytes(totalExpanded)}
+
+
+ +
+ Inspect {notebook.entries.length} package entries +
+ + + + + + + + + + + + {notebook.entries.map((entry) => ( + + + + + + + + ))} + +
Safe pathKindExpandedCompressionParse
+ {entry.normalizedPath} + {entry.path !== entry.normalizedPath ? ( + Original: {entry.path} + ) : null} + {entry.kind}{formatBytes(entry.uncompressedSize)}{entry.compressionMethod || 'unknown'} + + {entry.parseStatus} + +
+
+
+
+ ); +} diff --git a/src/components/PageReader.tsx b/src/components/PageReader.tsx new file mode 100644 index 0000000..342e584 --- /dev/null +++ b/src/components/PageReader.tsx @@ -0,0 +1,107 @@ +import type { OneNotePageDto } from '../onenote/model/dto.js'; + +interface PageReaderProps { + page?: OneNotePageDto; + loading: boolean; + error?: string; +} + +function formatDate(value: string | undefined): string | null { + if (!value) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return new Intl.DateTimeFormat(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(date); +} + +export function PageReader({ page, loading, error }: PageReaderProps) { + if (loading) { + return ( +
+

Loading extracted page text…

+
+ ); + } + + if (error) { + return ( +
+

Page unavailable

+

Could not read this page

+

{error}

+
+ ); + } + + if (!page) { + return ( +
+

Select a readable page to inspect its extracted text.

+
+ ); + } + + const createdAt = formatDate(page.createdAt); + const updatedAt = formatDate(page.updatedAt); + + return ( +
+
+

Selected page

+

{page.title || 'Untitled page'}

+ {createdAt || updatedAt ? ( +
+ {createdAt ? ( + <> +
Created
+
{createdAt}
+ + ) : null} + {updatedAt ? ( + <> +
Modified
+
{updatedAt}
+ + ) : null} +
+ ) : null} +
+ +
+ {page.blocks.length === 0 && page.text.length === 0 ? ( +

No readable plain text was extracted.

+ ) : page.blocks.length === 0 ? ( +

{page.text}

+ ) : ( + page.blocks.map((block, index) => { + switch (block.kind) { + case 'text': + return ( +

+ {block.text} +

+ ); + case 'line-break': + return
; + case 'link': + return ( +

+ {block.text} +

+ ); + case 'unsupported': + return ( + + ); + } + }) + )} +
+
+ ); +} diff --git a/src/components/SectionNavigation.tsx b/src/components/SectionNavigation.tsx new file mode 100644 index 0000000..4ae670e --- /dev/null +++ b/src/components/SectionNavigation.tsx @@ -0,0 +1,133 @@ +import type { + OneNotePackagedSectionDto, + OneNotePageSummaryDto, + OneNoteSectionDto, +} from '../onenote/model/dto.js'; + +interface PageListProps { + pages: OneNotePageSummaryDto[]; + selectedPageId?: string; + onSelectPage(pageId: string): void; +} + +function PageList({ pages, selectedPageId, onSelectPage }: PageListProps) { + if (pages.length === 0) { + return

No pages were extracted.

; + } + + return ( +
    + {pages.map((page) => ( +
  • + +
  • + ))} +
+ ); +} + +interface SingleSectionNavigationProps { + section: OneNoteSectionDto; + selectedPageId?: string; + onSelectPage(pageId: string): void; +} + +export function SingleSectionNavigation({ + section, + selectedPageId, + onSelectPage, +}: SingleSectionNavigationProps) { + return ( + + ); +} + +interface PackageNavigationProps { + sections: OneNotePackagedSectionDto[]; + selectedSectionId?: string; + selectedPageId?: string; + onSelectSection(sectionId: string): void; + onSelectPage(pageId: string): void; +} + +export function PackageNavigation({ + sections, + selectedSectionId, + selectedPageId, + onSelectSection, + onSelectPage, +}: PackageNavigationProps) { + const selected = sections.find((section) => section.id === selectedSectionId); + + return ( + + ); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..dd65a5e --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,14 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { App } from './App.js'; +import './styles.css'; + +const root = document.querySelector('#root'); +if (!root) throw new Error('Missing #root element'); + +createRoot(root).render( + + + +); diff --git a/src/onenote/binary/BinaryReader.ts b/src/onenote/binary/BinaryReader.ts new file mode 100644 index 0000000..f87f2f6 --- /dev/null +++ b/src/onenote/binary/BinaryReader.ts @@ -0,0 +1,158 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import { OneNoteParserError } from '../parser/error.js'; + +/** A bounded, little-endian reader. Every child view remains inside its range. */ +export class BinaryReader { + readonly bytes: Uint8Array; + readonly start: number; + readonly end: number; + readonly structure: string; + private cursor: number; + + constructor( + bytes: Uint8Array, + start = 0, + length = bytes.byteLength - start, + structure = 'binary data' + ) { + if ( + !Number.isSafeInteger(start) || + !Number.isSafeInteger(length) || + start < 0 || + length < 0 || + start + length > bytes.byteLength + ) { + throw new OneNoteParserError( + 'BOUNDS_EXCEEDED', + `Invalid ${structure} range (${start}, ${length}) for ${bytes.byteLength} bytes`, + { offset: start, structure } + ); + } + + this.bytes = bytes; + this.start = start; + this.end = start + length; + this.cursor = start; + this.structure = structure; + } + + get offset(): number { + return this.cursor; + } + + get relativeOffset(): number { + return this.cursor - this.start; + } + + get remaining(): number { + return this.end - this.cursor; + } + + seek(relativeOffset: number): void { + if ( + !Number.isSafeInteger(relativeOffset) || + relativeOffset < 0 || + relativeOffset > this.end - this.start + ) { + this.fail(`Invalid seek to relative offset ${relativeOffset}`); + } + this.cursor = this.start + relativeOffset; + } + + skip(length: number): void { + this.ensure(length); + this.cursor += length; + } + + u8(): number { + this.ensure(1); + return this.bytes[this.cursor++]!; + } + + u16(): number { + this.ensure(2); + const value = this.dataView(2).getUint16(0, true); + this.cursor += 2; + return value; + } + + u32(): number { + this.ensure(4); + const value = this.dataView(4).getUint32(0, true); + this.cursor += 4; + return value; + } + + u64(): bigint { + this.ensure(8); + const value = this.dataView(8).getBigUint64(0, true); + this.cursor += 8; + return value; + } + + read(length: number): Uint8Array { + this.ensure(length); + const value = this.bytes.subarray(this.cursor, this.cursor + length); + this.cursor += length; + return value; + } + + viewAt( + absoluteOffset: number, + length: number, + structure = this.structure + ): BinaryReader { + if ( + !Number.isSafeInteger(absoluteOffset) || + !Number.isSafeInteger(length) || + absoluteOffset < 0 || + length < 0 || + absoluteOffset + length > this.bytes.byteLength + ) { + throw new OneNoteParserError( + 'BOUNDS_EXCEEDED', + `${structure} range (${absoluteOffset}, ${length}) exceeds ${this.bytes.byteLength} bytes`, + { offset: absoluteOffset, structure } + ); + } + + return new BinaryReader(this.bytes, absoluteOffset, length, structure); + } + + subview(length: number, structure = this.structure): BinaryReader { + this.ensure(length); + const view = new BinaryReader(this.bytes, this.cursor, length, structure); + this.cursor += length; + return view; + } + + private ensure(length: number): void { + if ( + !Number.isSafeInteger(length) || + length < 0 || + length > this.remaining + ) { + this.fail( + `Tried to read ${String(length)} bytes with ${this.remaining} remaining` + ); + } + } + + private dataView(length: number): DataView { + return new DataView( + this.bytes.buffer, + this.bytes.byteOffset + this.cursor, + length + ); + } + + private fail(message: string): never { + throw new OneNoteParserError('BOUNDS_EXCEEDED', message, { + offset: this.cursor, + structure: this.structure, + }); + } +} diff --git a/src/onenote/binary/guid.ts b/src/onenote/binary/guid.ts new file mode 100644 index 0000000..5baca37 --- /dev/null +++ b/src/onenote/binary/guid.ts @@ -0,0 +1,28 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import type { BinaryReader } from './BinaryReader.js'; + +const HEX = Array.from({ length: 256 }, (_, value) => + value.toString(16).padStart(2, '0').toUpperCase() +); + +/** Read the mixed-endian GUID layout used by OneStore. */ +export function readGuid(reader: BinaryReader): string { + const bytes = reader.read(16); + const order = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15] as const; + const h = order.map((index) => HEX[bytes[index]!]!); + + return `{${h.slice(0, 4).join('')}-${h.slice(4, 6).join('')}-${h.slice(6, 8).join('')}-${h.slice(8, 10).join('')}-${h.slice(10).join('')}}`; +} + +export function normalizeGuid(value: string): string { + const plain = value.replace(/[{}-]/g, '').toUpperCase(); + if (!/^[0-9A-F]{32}$/.test(plain)) { + throw new TypeError(`Invalid GUID: ${value}`); + } + return `{${plain.slice(0, 8)}-${plain.slice(8, 12)}-${plain.slice(12, 16)}-${plain.slice(16, 20)}-${plain.slice(20)}}`; +} + +export const NIL_GUID = '{00000000-0000-0000-0000-000000000000}'; diff --git a/src/onenote/binary/time.ts b/src/onenote/binary/time.ts new file mode 100644 index 0000000..7f0ab25 --- /dev/null +++ b/src/onenote/binary/time.ts @@ -0,0 +1,35 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import { OneNoteParserError } from '../parser/error.js'; + +const FILETIME_UNIX_EPOCH_TICKS = 116_444_736_000_000_000n; +const TICKS_PER_MILLISECOND = 10_000n; +const SECONDS_1980_TO_UNIX = 315_532_800; + +export function filetimeToIso(value: bigint, structure: string): string { + const unixMilliseconds = + (value - FILETIME_UNIX_EPOCH_TICKS) / TICKS_PER_MILLISECOND; + const numeric = Number(unixMilliseconds); + if (!Number.isSafeInteger(numeric)) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `FILETIME is outside the supported date range: ${value}`, + { structure } + ); + } + const date = new Date(numeric); + if (Number.isNaN(date.getTime())) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Invalid FILETIME: ${value}`, + { structure } + ); + } + return date.toISOString(); +} + +export function oneNoteTimeToIso(value: number): string { + return new Date((value + SECONDS_1980_TO_UNIX) * 1000).toISOString(); +} diff --git a/src/onenote/index.ts b/src/onenote/index.ts new file mode 100644 index 0000000..c16dc81 --- /dev/null +++ b/src/onenote/index.ts @@ -0,0 +1,15 @@ +export { + detectOneNoteFormat, + type OneNoteFormatDetection, + type OneNoteFormatKind, +} from './parser/detect-format.js'; +export { OneNoteParserError } from './parser/error.js'; +export { + DEFAULT_ONENOTE_PARSER_LIMITS, + type OneNoteParserLimits, +} from './parser/limits.js'; +export { + parseOneNoteSection, + type ParseOneNoteSectionOptions, +} from './parser/parse-section.js'; +export type { OneNotePageSummaryDto, OneNoteSectionDto } from './model/dto.js'; diff --git a/src/onenote/model/diagnostics.ts b/src/onenote/model/diagnostics.ts new file mode 100644 index 0000000..e7e13bc --- /dev/null +++ b/src/onenote/model/diagnostics.ts @@ -0,0 +1,9 @@ +export interface ParserDiagnostic { + severity: 'warning' | 'error'; + code: string; + message: string; + offset?: number; + structure?: string; + propertyId?: string; + recoverable: boolean; +} diff --git a/src/onenote/model/dto.ts b/src/onenote/model/dto.ts new file mode 100644 index 0000000..306bc96 --- /dev/null +++ b/src/onenote/model/dto.ts @@ -0,0 +1,68 @@ +import type { ParserDiagnostic } from './diagnostics.js'; + +export interface OneNotePageSummaryDto { + id: string; + title: string; + createdAt?: string; + updatedAt?: string; + level?: number; + /** Complete plain text extracted for this page. */ + text: string; + textPreview: string; +} + +export type OneNoteContentBlockDto = + | { kind: 'text'; text: string } + | { kind: 'line-break' } + | { kind: 'link'; text: string; href?: string } + | { + kind: 'unsupported'; + sourceKind: string; + description: string; + }; + +export interface OneNotePageDto extends OneNotePageSummaryDto { + blocks: OneNoteContentBlockDto[]; + diagnostics: ParserDiagnostic[]; +} + +export interface OneNoteSectionDto { + format: 'one'; + sourceName: string; + sectionName?: string; + pages: OneNotePageSummaryDto[]; + diagnostics: ParserDiagnostic[]; + parserInfo: { + implementation: 'typescript'; + supportedVariant: string; + referenceRevision: string; + }; +} + +export interface OneNotePackageEntryDto { + path: string; + normalizedPath: string; + kind: 'section' | 'table-of-contents' | 'other'; + compressedSize?: number; + uncompressedSize: number; + compressionMethod?: string; + parseStatus: 'not-requested' | 'parsed' | 'unsupported' | 'failed'; +} + +export interface OneNotePackageDto { + format: 'onepkg'; + sourceName: string; + sourceSize: number; + entries: OneNotePackageEntryDto[]; + sections: OneNotePackagedSectionDto[]; + diagnostics: ParserDiagnostic[]; +} + +export interface OneNotePackagedSectionDto { + id: string; + path: string; + displayName: string; + parseStatus: OneNotePackageEntryDto['parseStatus']; + section?: OneNoteSectionDto; + diagnostics: ParserDiagnostic[]; +} diff --git a/src/onenote/one/constants.ts b/src/onenote/one/constants.ts new file mode 100644 index 0000000..bbc813b --- /dev/null +++ b/src/onenote/one/constants.ts @@ -0,0 +1,47 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 +// +// IDs selected from MS-ONE and onenote.rs (MPL-2.0), revision +// 5138a39a3f4e72b840932f9872fecde52fa9da60: `onestore/shared/jcid.rs`, +// `one/property/mod.rs`, and `one/property_set/*.rs`. + +export const JCID = { + SectionNode: 0x00060007, + PageSeriesNode: 0x00060008, + PageNode: 0x0006000b, + OutlineNode: 0x0006000c, + OutlineElementNode: 0x0006000d, + RichTextNode: 0x0006000e, + ImageNode: 0x00060011, + InkContainer: 0x00060014, + OutlineGroup: 0x00060019, + TableNode: 0x00060022, + TitleNode: 0x0006002c, + EmbeddedFileNode: 0x00060035, + PageManifestNode: 0x00060037, + PageMetadata: 0x00020030, + SectionMetadata: 0x00020031, + ParagraphStyleObject: 0x0012004d, +} as const; + +export const PROPERTY = { + NotebookManagementEntityGuid: 0x1c001c30, + SectionDisplayName: 0x1c00349b, + ElementChildNodes: 0x24001c20, + ChildGraphSpaceElementNodes: 0x2c001d63, + MetaDataObjectsAboveGraphSpace: 0x24003442, + ContentChildNodes: 0x24001c1f, + StructureElementChildNodes: 0x24001d5f, + TopologyCreationTimeStamp: 0x18001c65, + LastModifiedTime: 0x14001d7a, + PageLevel: 0x14001dff, + CachedTitleString: 0x1c001cf3, + CachedTitleStringFromPage: 0x1c001d3c, + RichEditTextUnicode: 0x1c001c22, + TextExtendedAscii: 0x1c003498, + TextRunFormatting: 0x24001e13, + TextRunIndex: 0x1c001e12, + Hidden: 0x08001e16, +} as const; diff --git a/src/onenote/one/section.ts b/src/onenote/one/section.ts new file mode 100644 index 0000000..2b8f8fd --- /dev/null +++ b/src/onenote/one/section.ts @@ -0,0 +1,661 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 +// +// Portions adapted from onenote.rs (MPL-2.0), revision +// 5138a39a3f4e72b840932f9872fecde52fa9da60: `onenote/{section,page_series, +// page,outline,rich_text}.rs` and the corresponding `one/property_set/*.rs` +// modules. Deviations: title/plain-text traversal only, bounded graph walking, +// serializable DTOs, and diagnostics for unsupported content. + +import { BinaryReader } from '../binary/BinaryReader.js'; +import { filetimeToIso, oneNoteTimeToIso } from '../binary/time.js'; +import type { ParserDiagnostic } from '../model/diagnostics.js'; +import type { OneNotePageSummaryDto, OneNoteSectionDto } from '../model/dto.js'; +import type { OneNoteParserLimits } from '../parser/limits.js'; +import { + countObjectReferences, + countObjectSpaceReferences, + findProperty, + referenceOffset, +} from '../onestore/property-set.js'; +import type { DesktopOneStore } from '../onestore/desktop-store.js'; +import type { + ExGuid, + ObjectSpace, + PropertyEntry, + StoreObject, +} from '../onestore/types.js'; +import { exGuidKey } from '../onestore/types.js'; +import { JCID, PROPERTY } from './constants.js'; + +interface OneContext { + store: DesktopOneStore; + limits: OneNoteParserLimits; + diagnostics: ParserDiagnostic[]; + extractedChars: number; + currentPage?: string; +} + +type PageWithTextDto = OneNotePageSummaryDto & { text: string }; + +export function buildSectionDto( + store: DesktopOneStore, + sourceName: string, + limits: OneNoteParserLimits, + diagnostics: ParserDiagnostic[] +): OneNoteSectionDto { + const context: OneContext = { + store, + limits, + diagnostics, + extractedChars: 0, + }; + const root = store.rootObjectSpace; + const metadata = rootObject(root, 2, 'section metadata root'); + assertJcid(metadata, JCID.SectionMetadata, 'SectionMetadata'); + const displayName = optionalString(metadata, PROPERTY.SectionDisplayName); + + const section = rootObject(root, 1, 'section content root'); + assertJcid(section, JCID.SectionNode, 'SectionNode'); + const pageSeriesIds = objectReferences(section, PROPERTY.ElementChildNodes); + const pages: PageWithTextDto[] = []; + + for (const seriesId of pageSeriesIds) { + const series = getObject(root, seriesId, 'PageSeriesNode'); + assertJcid(series, JCID.PageSeriesNode, 'PageSeriesNode'); + const pageSpaceIds = objectSpaceReferences( + series, + PROPERTY.ChildGraphSpaceElementNodes + ); + for (const pageSpaceId of pageSpaceIds) { + context.currentPage = undefined; + const pageSpace = store.objectSpaces.get(exGuidKey(pageSpaceId)); + if (!pageSpace) { + addDiagnostic( + context, + 'MISSING_PAGE_SPACE', + `Page object space ${exGuidKey(pageSpaceId)} is missing`, + 'PageSeriesNode' + ); + continue; + } + + try { + if (pages.length >= limits.maxPages) { + addDiagnostic( + context, + 'PAGE_LIMIT_REACHED', + `Page count exceeds ${limits.maxPages}`, + 'PageSeriesNode', + 'error' + ); + break; + } + pages.push(parsePage(context, pageSpace)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + addDiagnostic( + context, + 'PAGE_PARSE_FAILED', + `Could not parse page ${exGuidKey(pageSpaceId)}: ${message}`, + 'Page', + 'error' + ); + } + } + if (pages.length >= limits.maxPages) break; + } + + return { + format: 'one', + sourceName, + sectionName: displayName ?? stripOneExtension(sourceName), + pages, + diagnostics, + parserInfo: { + implementation: 'typescript', + supportedVariant: 'desktop-revision-store', + referenceRevision: 'onenote.rs@5138a39a3f4e72b840932f9872fecde52fa9da60', + }, + }; +} + +function parsePage(context: OneContext, space: ObjectSpace): PageWithTextDto { + const metadata = rootObject(space, 2, 'page metadata root'); + assertJcid(metadata, JCID.PageMetadata, 'PageMetadata'); + const pageGuid = requiredGuid( + metadata, + PROPERTY.NotebookManagementEntityGuid, + 'PageMetadata entity GUID' + ); + context.currentPage = pageGuid; + const created = optionalU64(metadata, PROPERTY.TopologyCreationTimeStamp); + const level = optionalU32(metadata, PROPERTY.PageLevel); + const cachedMetadataTitle = optionalString( + metadata, + PROPERTY.CachedTitleString + ); + + const manifest = rootObject(space, 1, 'page content root'); + assertJcid(manifest, JCID.PageManifestNode, 'PageManifestNode'); + const pageId = objectReferences(manifest, PROPERTY.ContentChildNodes)[0]; + if (!pageId) throw new Error('PageManifestNode has no page reference'); + const page = getObject(space, pageId, 'PageNode'); + assertJcid(page, JCID.PageNode, 'PageNode'); + + const updated = optionalU32(page, PROPERTY.LastModifiedTime); + const cachedPageTitle = optionalString( + page, + PROPERTY.CachedTitleStringFromPage + ); + const titleId = objectReferences( + page, + PROPERTY.StructureElementChildNodes + )[0]; + const contentIds = objectReferences(page, PROPERTY.ElementChildNodes); + + let titleText: string | undefined; + if (titleId) { + try { + const title = getObject(space, titleId, 'TitleNode'); + assertJcid(title, JCID.TitleNode, 'TitleNode'); + const titleOutlines = objectReferences(title, PROPERTY.ElementChildNodes); + for (const outlineId of titleOutlines) { + const paragraphs = extractGraphTextSafely( + context, + space, + outlineId, + 'title' + ); + titleText = paragraphs.map(cleanTitle).find((item) => item.length > 0); + if (titleText) break; + } + } catch (error) { + addDiagnostic( + context, + 'TITLE_PARSE_FAILED', + error instanceof Error ? error.message : String(error), + 'TitleNode' + ); + } + } + + const bodyParagraphs: string[] = []; + for (const contentId of contentIds) { + bodyParagraphs.push( + ...extractGraphTextSafely(context, space, contentId, 'page content') + ); + } + let text = bodyParagraphs + .map((paragraph) => paragraph.split('\u000b').join('\n').trimEnd()) + .filter((paragraph) => paragraph.length > 0) + .join('\n'); + const remaining = + context.limits.maxExtractedTextChars - context.extractedChars; + if (text.length > remaining) { + text = text.slice(0, Math.max(0, remaining)); + addDiagnostic( + context, + 'TEXT_LIMIT_REACHED', + `Extracted text was truncated at ${context.limits.maxExtractedTextChars} characters`, + `Page ${pageGuid}` + ); + } + context.extractedChars += text.length; + + const bodyFallback = bodyParagraphs + .map(cleanTitle) + .find((item) => item.length > 0); + const title = + titleText ?? + bodyFallback ?? + cachedPageTitle ?? + cachedMetadataTitle ?? + 'Untitled page'; + + return { + id: pageGuid, + title, + createdAt: + created === undefined ? undefined : safeFiletime(context, created), + updatedAt: updated === undefined ? undefined : oneNoteTimeToIso(updated), + level, + text, + textPreview: preview(text), + }; +} + +function extractGraphTextSafely( + context: OneContext, + space: ObjectSpace, + id: ExGuid, + label: string +): string[] { + try { + return extractGraphText(context, space, id, new Set(), 0); + } catch (error) { + addDiagnostic( + context, + 'CONTENT_PARSE_FAILED', + `Could not extract ${label} ${exGuidKey(id)}: ${error instanceof Error ? error.message : String(error)}`, + 'MS-ONE content graph' + ); + return []; + } +} + +function extractGraphText( + context: OneContext, + space: ObjectSpace, + id: ExGuid, + active: Set, + depth: number +): string[] { + if (depth > context.limits.maxGraphDepth) { + addDiagnostic( + context, + 'GRAPH_DEPTH_LIMIT', + `Content graph depth exceeds ${context.limits.maxGraphDepth}`, + 'MS-ONE content graph' + ); + return []; + } + const key = exGuidKey(id); + if (active.has(key)) { + addDiagnostic( + context, + 'CONTENT_GRAPH_CYCLE', + `Cycle detected at object ${key}`, + 'MS-ONE content graph' + ); + return []; + } + + const object = space.objects.get(key); + if (!object) { + addDiagnostic( + context, + 'MISSING_CONTENT_OBJECT', + `Content object ${key} is missing`, + 'MS-ONE content graph' + ); + return []; + } + + active.add(key); + try { + if (object.jcid === JCID.RichTextNode) { + const text = richText(context, space, object); + return text === undefined ? [] : [text]; + } + if (object.jcid === JCID.OutlineNode || object.jcid === JCID.OutlineGroup) { + return traverseReferences( + context, + space, + objectReferences(object, PROPERTY.ElementChildNodes), + active, + depth + ); + } + if (object.jcid === JCID.OutlineElementNode) { + const contents = traverseReferences( + context, + space, + objectReferences(object, PROPERTY.ContentChildNodes), + active, + depth + ); + const children = traverseReferences( + context, + space, + objectReferences(object, PROPERTY.ElementChildNodes), + active, + depth + ); + return [...contents, ...children]; + } + + addDiagnostic( + context, + unsupportedCode(object.jcid), + `Content object JCID 0x${object.jcid.toString(16)} is not yet rendered`, + `Object ${key}` + ); + return []; + } finally { + active.delete(key); + } +} + +function traverseReferences( + context: OneContext, + space: ObjectSpace, + ids: ExGuid[], + active: Set, + depth: number +): string[] { + const result: string[] = []; + for (const id of ids) { + result.push(...extractGraphText(context, space, id, active, depth + 1)); + } + return result; +} + +function richText( + context: OneContext, + space: ObjectSpace, + object: StoreObject +): string | undefined { + let text = optionalString(object, PROPERTY.RichEditTextUnicode); + if (text === undefined) + text = optionalLatin1(object, PROPERTY.TextExtendedAscii); + if (text === undefined) return undefined; + const styles = objectReferences(object, PROPERTY.TextRunFormatting); + if (styles.length === 0) return text.split('\0').join(''); + const indices = optionalU32Vector(object, PROPERTY.TextRunIndex) ?? []; + const hidden = styles.map((styleId) => { + const style = space.objects.get(exGuidKey(styleId)); + if (!style) { + addDiagnostic( + context, + 'MISSING_TEXT_STYLE', + `Text-run style ${exGuidKey(styleId)} is missing`, + 'RichTextNode' + ); + return false; + } + return optionalBool(style, PROPERTY.Hidden) ?? false; + }); + + const boundaries = [0, ...indices]; + if (boundaries.length === hidden.length) boundaries.push(text.length); + let visible = ''; + for (let index = 0; index < hidden.length; index += 1) { + if (hidden[index]) continue; + const start = Math.min(boundaries[index] ?? text.length, text.length); + const end = Math.min(boundaries[index + 1] ?? text.length, text.length); + if (start < end) visible += text.slice(start, end); + } + // OneNote producers sometimes retain UTF-16 terminators inside text vectors. + return visible.split('\0').join(''); +} + +function objectReferences(object: StoreObject, propertyId: number): ExGuid[] { + const property = findProperty(object.props, propertyId); + if (!property) return []; + let count: number; + if (property.value.kind === 'objectId') count = 1; + else if (property.value.kind === 'objectIds') count = property.value.count; + else + throw new Error( + `Property 0x${propertyId.toString(16)} is not an object reference` + ); + + const offset = referenceOffset( + object.props, + propertyId, + countObjectReferences + ); + return resolveReferenceSlice(object, object.props.objectIds, offset, count); +} + +function objectSpaceReferences( + object: StoreObject, + propertyId: number +): ExGuid[] { + const property = findProperty(object.props, propertyId); + if (!property) return []; + let count: number; + if (property.value.kind === 'objectSpaceId') count = 1; + else if (property.value.kind === 'objectSpaceIds') + count = property.value.count; + else { + throw new Error( + `Property 0x${propertyId.toString(16)} is not an object-space reference` + ); + } + const offset = referenceOffset( + object.props, + propertyId, + countObjectSpaceReferences + ); + return resolveReferenceSlice( + object, + object.props.objectSpaceIds, + offset, + count + ); +} + +function resolveReferenceSlice( + object: StoreObject, + ids: { guidIndex: number; value: number }[], + offset: number, + count: number +): ExGuid[] { + if (offset + count > ids.length) { + throw new Error( + `Reference range ${offset}+${count} exceeds stream length ${ids.length}` + ); + } + return ids.slice(offset, offset + count).map((id) => { + const guid = object.mapping.get(id.guidIndex); + if (!guid) throw new Error(`GUID mapping ${id.guidIndex} is missing`); + return { guid, value: id.value }; + }); +} + +function rootObject( + space: ObjectSpace, + role: number, + structure: string +): StoreObject { + const id = space.roots.get(role); + if (!id) throw new Error(`Object space has no ${structure}`); + return getObject(space, id, structure); +} + +function getObject( + space: ObjectSpace, + id: ExGuid, + structure: string +): StoreObject { + const object = space.objects.get(exGuidKey(id)); + if (!object) + throw new Error(`${structure} object ${exGuidKey(id)} is missing`); + return object; +} + +function assertJcid( + object: StoreObject, + expected: number, + structure: string +): void { + if (object.jcid !== expected) { + throw new Error( + `${structure} has JCID 0x${object.jcid.toString(16)}, expected 0x${expected.toString(16)}` + ); + } +} + +function optionalEntry( + object: StoreObject, + propertyId: number +): PropertyEntry | undefined { + return findProperty(object.props, propertyId); +} + +function optionalString( + object: StoreObject, + propertyId: number +): string | undefined { + const entry = optionalEntry(object, propertyId); + if (!entry) return undefined; + if (entry.value.kind !== 'bytes') { + throw new Error( + `Property 0x${propertyId.toString(16)} is not a byte vector` + ); + } + if (entry.value.value.byteLength % 2 !== 0) { + throw new Error( + `UTF-16 property 0x${propertyId.toString(16)} has odd length` + ); + } + return new TextDecoder('utf-16le', { fatal: true }).decode(entry.value.value); +} + +function optionalLatin1( + object: StoreObject, + propertyId: number +): string | undefined { + const entry = optionalEntry(object, propertyId); + if (!entry) return undefined; + if (entry.value.kind !== 'bytes') { + throw new Error( + `Property 0x${propertyId.toString(16)} is not a byte vector` + ); + } + let result = ''; + for (const value of entry.value.value) result += String.fromCharCode(value); + return result; +} + +function requiredGuid( + object: StoreObject, + propertyId: number, + structure: string +): string { + const entry = optionalEntry(object, propertyId); + if ( + !entry || + entry.value.kind !== 'bytes' || + entry.value.value.length !== 16 + ) { + throw new Error(`${structure} is absent or not 16 bytes`); + } + const reader = new BinaryReader(entry.value.value, 0, 16, structure); + const bytes = reader.read(16); + const order = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]; + const hex = order.map((index) => bytes[index]!.toString(16).padStart(2, '0')); + return `{${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}}`.toUpperCase(); +} + +function optionalBool( + object: StoreObject, + propertyId: number +): boolean | undefined { + const value = optionalEntry(object, propertyId)?.value; + if (!value) return undefined; + if (value.kind !== 'bool') throw new Error('Expected a Boolean property'); + return value.value; +} + +function optionalU32( + object: StoreObject, + propertyId: number +): number | undefined { + const value = optionalEntry(object, propertyId)?.value; + if (!value) return undefined; + if (value.kind !== 'u32') throw new Error('Expected a u32 property'); + return value.value; +} + +function optionalU64( + object: StoreObject, + propertyId: number +): bigint | undefined { + const value = optionalEntry(object, propertyId)?.value; + if (!value) return undefined; + if (value.kind !== 'u64') throw new Error('Expected a u64 property'); + return value.value; +} + +function optionalU32Vector( + object: StoreObject, + propertyId: number +): number[] | undefined { + const value = optionalEntry(object, propertyId)?.value; + if (!value) return undefined; + if (value.kind !== 'bytes' || value.value.byteLength % 4 !== 0) { + throw new Error('Expected a u32 vector property'); + } + const reader = new BinaryReader( + value.value, + 0, + value.value.byteLength, + 'u32 vector' + ); + const result: number[] = []; + while (reader.remaining > 0) result.push(reader.u32()); + return result; +} + +function safeFiletime(context: OneContext, value: bigint): string | undefined { + try { + return filetimeToIso(value, 'PageMetadata creation time'); + } catch (error) { + addDiagnostic( + context, + 'INVALID_PAGE_TIMESTAMP', + error instanceof Error ? error.message : String(error), + 'PageMetadata' + ); + return undefined; + } +} + +function cleanTitle(text: string): string { + const marker = '\ufddfHYPERLINK "'; + let result = text; + while (result.startsWith('\u000b')) result = result.slice(1); + result = result.trim(); + let start = result.indexOf(marker); + while (start >= 0) { + const valueStart = start + marker.length; + const quote = result.indexOf('"', valueStart); + result = + quote < 0 + ? result.slice(0, start) + : result.slice(0, start) + result.slice(quote + 1); + start = result.indexOf(marker); + } + return result.trim(); +} + +function preview(text: string): string { + const compact = text.replace(/\s+/g, ' ').trim(); + return compact.length <= 500 ? compact : `${compact.slice(0, 497)}…`; +} + +function unsupportedCode(jcid: number): string { + if (jcid === JCID.ImageNode) return 'UNSUPPORTED_IMAGE'; + if (jcid === JCID.TableNode) return 'UNSUPPORTED_TABLE'; + if (jcid === JCID.EmbeddedFileNode) return 'UNSUPPORTED_EMBEDDED_FILE'; + if (jcid === JCID.InkContainer) return 'UNSUPPORTED_INK'; + return 'UNSUPPORTED_CONTENT_OBJECT'; +} + +function addDiagnostic( + context: OneContext, + code: string, + message: string, + structure: string, + severity: 'warning' | 'error' = 'warning' +): void { + if (context.diagnostics.length >= context.limits.maxDiagnostics) return; + context.diagnostics.push({ + severity, + code, + message, + structure: context.currentPage + ? `${structure} (page ${context.currentPage})` + : structure, + recoverable: true, + }); +} + +function stripOneExtension(filename: string): string { + return (filename.split(/[\\/]/).at(-1) ?? filename).replace(/\.one$/i, ''); +} diff --git a/src/onenote/onepkg/binary-reader.ts b/src/onenote/onepkg/binary-reader.ts new file mode 100644 index 0000000..3bcc7b5 --- /dev/null +++ b/src/onenote/onepkg/binary-reader.ts @@ -0,0 +1,86 @@ +import { fail } from './errors.js'; + +export class BinaryReader { + readonly bytes: Uint8Array; + readonly end: number; + offset: number; + private readonly view: DataView; + + constructor(bytes: Uint8Array, offset = 0, end = bytes.byteLength) { + if (offset < 0 || end < offset || end > bytes.byteLength) { + throw new RangeError('Invalid BinaryReader range'); + } + this.bytes = bytes; + this.offset = offset; + this.end = end; + this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + } + + ensure(length: number, structure: string): void { + if ( + !Number.isSafeInteger(length) || + length < 0 || + this.offset > this.end - length + ) { + fail( + 'cab-unexpected-eof', + `Unexpected end of cabinet while reading ${structure}.`, + { offset: this.offset, structure } + ); + } + } + + readU8(structure: string): number { + this.ensure(1, structure); + return this.bytes[this.offset++]!; + } + + readU16(structure: string): number { + this.ensure(2, structure); + const value = this.view.getUint16(this.offset, true); + this.offset += 2; + return value; + } + + readU32(structure: string): number { + this.ensure(4, structure); + const value = this.view.getUint32(this.offset, true); + this.offset += 4; + return value; + } + + readBytes(length: number, structure: string): Uint8Array { + this.ensure(length, structure); + const result = this.bytes.subarray(this.offset, this.offset + length); + this.offset += length; + return result; + } + + skip(length: number, structure: string): void { + this.ensure(length, structure); + this.offset += length; + } + + readNullTerminatedBytes(maxBytes: number, structure: string): Uint8Array { + const start = this.offset; + while (this.offset < this.end && this.bytes[this.offset] !== 0) { + if (this.offset - start >= maxBytes) { + fail( + 'cab-name-too-long', + `Cabinet string exceeds the configured ${maxBytes}-byte limit.`, + { offset: start, structure } + ); + } + this.offset += 1; + } + if (this.offset >= this.end) { + fail('cab-unterminated-string', 'Cabinet string is not NUL-terminated.', { + offset: start, + structure, + }); + } + const value = this.bytes.subarray(start, this.offset); + this.offset += 1; + return value; + } +} diff --git a/src/onenote/onepkg/cabinet.test.ts b/src/onenote/onepkg/cabinet.test.ts new file mode 100644 index 0000000..d4bb09d --- /dev/null +++ b/src/onenote/onepkg/cabinet.test.ts @@ -0,0 +1,200 @@ +import { describe, expect, it } from 'vitest'; + +import type { OneNoteSectionDto } from '../model/dto.js'; +import { enumerateCabinet, extractCabinet } from './cabinet.js'; +import { OnePkgError } from './errors.js'; +import { LzxDecoder } from './lzx/decoder.js'; +import { openOneNotePackage } from './package.js'; + +/* + * These byte-for-byte CAB fixtures are copied from rust-cab's MIT-licensed + * `src/cabinet.rs` tests at commit + * c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2. + * Copyright (c) 2017 Matthew D. Steele. + */ +function binary(value: string): Uint8Array { + return Uint8Array.from(value, (character) => character.charCodeAt(0)); +} + +const UNCOMPRESSED_CAB = binary( + 'MSCF\0\0\0\0\x59\0\0\0\0\0\0\0' + + '\x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x01\0\0\0\x34\x12\0\0' + + '\x43\0\0\0\x01\0\0\0' + + '\x0e\0\0\0\0\0\0\0\0\0\x6c\x22\xba\x59\x01\0hi.txt\0' + + '\x4c\x1a\x2e\x7f\x0e\0\x0e\0Hello, world!\n' +); + +const TWO_FILE_UNCOMPRESSED_CAB = binary( + 'MSCF\0\0\0\0\x80\0\0\0\0\0\0\0' + + '\x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x02\0\0\0\x34\x12\0\0' + + '\x5b\0\0\0\x01\0\0\0' + + '\x0e\0\0\0\0\0\0\0\0\0\x6c\x22\xe7\x59\x01\0hi.txt\0' + + '\x0f\0\0\0\x0e\0\0\0\0\0\x6c\x22\xe7\x59\x01\0bye.txt\0' + + '\0\0\0\0\x1d\0\x1d\0Hello, world!\nSee you later!\n' +); + +const LZX_CAB = binary( + '\x4d\x53\x43\x46\x00\x00\x00\x00\x97\x00\x00\x00\x00\x00\x00' + + '\x00\x2c\x00\x00\x00\x00\x00\x00\x00\x03\x01\x01\x00\x02\x00' + + '\x00\x00\x2d\x05\x00\x00\x5b\x00\x00\x00\x01\x00\x03\x13\x0f' + + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x21\x53\x0d\xb2\x20\x00' + + '\x68\x69\x2e\x74\x78\x74\x00\x10\x00\x00\x00\x0f\x00\x00\x00' + + '\x00\x00\x21\x53\x0b\xb2\x20\x00\x62\x79\x65\x2e\x74\x78\x74' + + '\x00\x5c\xef\x2a\xc7\x34\x00\x1f\x00\x5b\x80\x80\x8d\x00\x30' + + '\xf0\x01\x10\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x48' + + '\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21\x0d\x0a\x53' + + '\x65\x65\x20\x79\x6f\x75\x20\x6c\x61\x74\x65\x72\x21\x0d\x0a' + + '\x00' +); + +function findAscii(bytes: Uint8Array, value: string): number { + const needle = new TextEncoder().encode(value); + outer: for ( + let offset = 0; + offset <= bytes.length - needle.length; + offset += 1 + ) { + for (let index = 0; index < needle.length; index += 1) { + if (bytes[offset + index] !== needle[index]) continue outer; + } + return offset; + } + throw new Error(`Could not find ${value}`); +} + +function expectCode(action: () => unknown, code: string): void { + try { + action(); + } catch (error) { + expect(error).toBeInstanceOf(OnePkgError); + expect((error as OnePkgError).diagnostic.code).toBe(code); + return; + } + throw new Error(`Expected OnePkgError ${code}`); +} + +describe('CAB enumeration and extraction', () => { + it('enumerates and extracts rust-cab uncompressed data', async () => { + const listing = enumerateCabinet(UNCOMPRESSED_CAB); + expect(listing.entries).toHaveLength(1); + expect(listing.entries[0]).toMatchObject({ + normalizedPath: 'hi.txt', + uncompressedSize: 14, + compression: { kind: 'none' }, + }); + + const extracted = await extractCabinet(UNCOMPRESSED_CAB); + expect(new TextDecoder().decode(extracted.extractedEntries[0]!.bytes)).toBe( + 'Hello, world!\n' + ); + }); + + it('extracts the rust-cab LZX 0x1303 fixture', async () => { + const listing = enumerateCabinet(LZX_CAB); + expect(listing.folders[0]!.compression).toMatchObject({ + kind: 'lzx', + raw: 0x1303, + windowBits: 19, + }); + + const extracted = await extractCabinet(LZX_CAB); + expect( + extracted.extractedEntries.map((entry) => [ + entry.normalizedPath, + new TextDecoder().decode(entry.bytes), + ]) + ).toEqual([ + ['hi.txt', 'Hello, world!\r\n'], + ['bye.txt', 'See you later!\r\n'], + ]); + }); + + it('retains an uncompressed LZX block across CAB chunks', () => { + const decoder = new LzxDecoder(18, { maxOperations: 100 }); + const first = binary( + '\x00\x30\x70\x00' + '\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00abc' + ); + expect(new TextDecoder().decode(decoder.decompressNext(first, 3))).toBe( + 'abc' + ); + expect( + new TextDecoder().decode(decoder.decompressNext(binary('defg'), 4)) + ).toBe('defg'); + decoder.finish(); + }); + + it('rejects checksum corruption', async () => { + const corrupted = UNCOMPRESSED_CAB.slice(); + corrupted[corrupted.length - 1] = corrupted[corrupted.length - 1]! ^ 1; + await expect(extractCabinet(corrupted)).rejects.toMatchObject({ + diagnostic: { code: 'cab-checksum-mismatch' }, + }); + }); + + it('rejects traversal paths and case-insensitive duplicate paths', () => { + const traversal = UNCOMPRESSED_CAB.slice(); + traversal.set(binary('../x.x'), findAscii(traversal, 'hi.txt')); + expectCode(() => enumerateCabinet(traversal), 'cab-path-invalid-segment'); + + const duplicate = TWO_FILE_UNCOMPRESSED_CAB.slice(); + duplicate.set(binary('HI.TXT\0\0'), findAscii(duplicate, 'bye.txt')); + expectCode(() => enumerateCabinet(duplicate), 'cab-duplicate-path'); + }); + + it('reports an out-of-range folder data offset as a CAB diagnostic', () => { + const malformed = UNCOMPRESSED_CAB.slice(); + new DataView( + malformed.buffer, + malformed.byteOffset, + malformed.byteLength + ).setUint32(36, malformed.length + 1, true); + expectCode(() => enumerateCabinet(malformed), 'cab-invalid-data-offset'); + }); + + it('enforces compression-ratio limits and cancellation', () => { + expectCode( + () => + enumerateCabinet(UNCOMPRESSED_CAB, { + limits: { maxCompressionRatio: 0.5 }, + }), + 'cab-compression-ratio-limit' + ); + expectCode( + () => + enumerateCabinet(UNCOMPRESSED_CAB, { + cancellation: { isCancelled: () => true }, + }), + 'onepkg-cancelled' + ); + }); + + it('keeps section parsing behind the injected callback boundary', async () => { + const oneCab = UNCOMPRESSED_CAB.slice(); + oneCab.set(binary('hi.one'), findAscii(oneCab, 'hi.txt')); + const section: OneNoteSectionDto = { + format: 'one', + sourceName: 'hi.one', + pages: [], + diagnostics: [], + parserInfo: { + implementation: 'typescript', + supportedVariant: 'test', + referenceRevision: 'test', + }, + }; + const result = await openOneNotePackage(oneCab, { + sourceName: 'fixture.onepkg', + parseSection: ({ normalizedPath, bytes }) => { + expect(normalizedPath).toBe('hi.one'); + expect(bytes).toHaveLength(14); + return { status: 'parsed', value: section }; + }, + }); + expect(result.package).toMatchObject({ + sourceName: 'fixture.onepkg', + sourceSize: oneCab.length, + entries: [{ parseStatus: 'parsed' }], + sections: [{ path: 'hi.one', section }], + }); + }); +}); diff --git a/src/onenote/onepkg/cabinet.ts b/src/onenote/onepkg/cabinet.ts new file mode 100644 index 0000000..5c297bd --- /dev/null +++ b/src/onenote/onepkg/cabinet.ts @@ -0,0 +1,727 @@ +/* + * SPDX-License-Identifier: MIT + * + * CAB structure parsing and extraction translated from rust-cab 0.6.0. + * rust-cab commit c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2 + * Copyright (c) 2017 Matthew D. Steele, MIT License. + * Modified for immutable browser Uint8Array input, strict archive-path handling, + * explicit resource limits, cancellation, and whole-folder extraction. + */ + +import { BinaryReader } from './binary-reader.js'; +import { checkCancellation, yieldForCancellation } from './cancellation.js'; +import { calculateDataBlockChecksum } from './checksum.js'; +import { fail, warning } from './errors.js'; +import { LzxDecoder } from './lzx/decoder.js'; +import { decodeCabFileName, normalizeCabPath } from './path.js'; +import { + resolveLimits, + type CabCompressionMethod, + type CabEnumerationResult, + type CabExtractionResult, + type CabFileEntry, + type CabOpenOptions, + type OnePkgLimits, +} from './types.js'; + +const CAB_SIGNATURE = 0x4643_534d; +const FLAG_PREVIOUS_CABINET = 0x0001; +const FLAG_NEXT_CABINET = 0x0002; +const FLAG_RESERVE_PRESENT = 0x0004; +const ATTRIBUTE_NAME_IS_UTF8 = 0x0080; +const MAX_CAB_BLOCK_OUTPUT = 32 * 1024; + +interface CabDataBlock { + headerOffset: number; + checksum: number; + compressedSize: number; + uncompressedSize: number; + reserve: Uint8Array; + dataOffset: number; +} + +interface CabFolderInternal { + index: number; + firstDataBlockOffset: number; + dataBlockCount: number; + compression: CabCompressionMethod; + blocks: CabDataBlock[]; + compressedSize: number; + uncompressedSize: number; + dataEnd: number; +} + +interface ParsedCabinet { + bytes: Uint8Array; + limits: OnePkgLimits; + folders: CabFolderInternal[]; + result: CabEnumerationResult; +} + +function checkedAdd( + left: number, + right: number, + code: string, + structure: string +): number { + const sum = left + right; + if (!Number.isSafeInteger(sum)) { + fail(code, `Integer overflow while reading ${structure}.`, { structure }); + } + return sum; +} + +function parseCompression( + raw: number, + limits: OnePkgLimits, + offset: number +): CabCompressionMethod { + const kind = raw & 0x000f; + if (kind === 0) { + if (raw !== 0) { + fail( + 'cab-invalid-compression', + `Invalid uncompressed type 0x${raw.toString(16)}.`, + { + offset, + structure: 'CFFOLDER.typeCompress', + } + ); + } + return { kind: 'none', raw, label: 'None' }; + } + if (kind === 1) { + if (raw !== 1) { + fail( + 'cab-invalid-compression', + `Invalid MSZIP type 0x${raw.toString(16)}.`, + { + offset, + structure: 'CFFOLDER.typeCompress', + } + ); + } + return { kind: 'mszip', raw, label: 'MSZIP' }; + } + if (kind === 2) { + const level = (raw & 0x00f0) >>> 4; + const memoryBits = (raw & 0x1f00) >>> 8; + if (level < 1 || level > 7 || memoryBits < 10 || memoryBits > 21) { + fail( + 'cab-invalid-compression', + `Invalid Quantum type 0x${raw.toString(16)}.`, + { + offset, + structure: 'CFFOLDER.typeCompress', + } + ); + } + return { kind: 'quantum', raw, label: 'Quantum', level, memoryBits }; + } + if (kind === 3) { + if ((raw & ~0x1f0f) !== 0) { + fail( + 'cab-invalid-compression', + `Invalid LZX type 0x${raw.toString(16)}.`, + { + offset, + structure: 'CFFOLDER.typeCompress', + } + ); + } + const windowBits = (raw & 0x1f00) >>> 8; + if (windowBits < 15 || windowBits > 25) { + fail('cab-invalid-lzx-window', `Invalid LZX window 2^${windowBits}.`, { + offset, + structure: 'CFFOLDER.typeCompress', + }); + } + const windowBytes = 2 ** windowBits; + if (windowBytes > limits.maxLzxWindowBytes) { + fail( + 'cab-lzx-window-limit', + `LZX window ${windowBytes} exceeds the configured ${limits.maxLzxWindowBytes}-byte limit.`, + { offset, structure: 'CFFOLDER.typeCompress' } + ); + } + return { kind: 'lzx', raw, label: 'LZX', windowBits, windowBytes }; + } + fail( + 'cab-invalid-compression', + `Unknown CAB compression type 0x${raw.toString(16)}.`, + { + offset, + structure: 'CFFOLDER.typeCompress', + } + ); +} + +function parseCabinet( + bytes: Uint8Array, + options: CabOpenOptions +): ParsedCabinet { + if (!(bytes instanceof Uint8Array)) { + throw new TypeError('CAB input must be a Uint8Array'); + } + const limits = resolveLimits(options.limits); + if (bytes.byteLength > limits.maxCabinetBytes) { + fail( + 'cab-input-limit', + `Cabinet is ${bytes.byteLength} bytes; configured limit is ${limits.maxCabinetBytes}.`, + { structure: 'CFHEADER' } + ); + } + checkCancellation(options.cancellation); + + const header = new BinaryReader(bytes); + if (header.readU32('CFHEADER.signature') !== CAB_SIGNATURE) { + fail( + 'cab-invalid-signature', + 'Input does not begin with the MSCF CAB signature.', + { + offset: 0, + structure: 'CFHEADER.signature', + } + ); + } + const reserved1 = header.readU32('CFHEADER.reserved1'); + const declaredSize = header.readU32('CFHEADER.cbCabinet'); + const reserved2 = header.readU32('CFHEADER.reserved2'); + const firstFileOffset = header.readU32('CFHEADER.coffFiles'); + const reserved3 = header.readU32('CFHEADER.reserved3'); + const minorVersion = header.readU8('CFHEADER.versionMinor'); + const majorVersion = header.readU8('CFHEADER.versionMajor'); + const folderCount = header.readU16('CFHEADER.cFolders'); + const fileCount = header.readU16('CFHEADER.cFiles'); + const flags = header.readU16('CFHEADER.flags'); + const cabinetSetId = header.readU16('CFHEADER.setID'); + const cabinetSetIndex = header.readU16('CFHEADER.iCabinet'); + + if (declaredSize < 36 || declaredSize > bytes.byteLength) { + fail( + 'cab-invalid-size', + `Declared cabinet size ${declaredSize} is outside the available ${bytes.byteLength} bytes.`, + { offset: 8, structure: 'CFHEADER.cbCabinet' } + ); + } + if (declaredSize > limits.maxCabinetBytes) { + fail( + 'cab-input-limit', + 'Declared cabinet size exceeds the configured limit.', + { + offset: 8, + structure: 'CFHEADER.cbCabinet', + } + ); + } + if (majorVersion > 1 || (majorVersion === 1 && minorVersion > 3)) { + fail( + 'cab-version-unsupported', + `CAB version ${majorVersion}.${minorVersion} is not supported.`, + { offset: 24, structure: 'CFHEADER.version' } + ); + } + if ((flags & ~0x0007) !== 0) { + fail( + 'cab-invalid-flags', + `Unknown CAB header flags 0x${flags.toString(16)}.`, + { + offset: 30, + structure: 'CFHEADER.flags', + } + ); + } + if ((flags & (FLAG_PREVIOUS_CABINET | FLAG_NEXT_CABINET)) !== 0) { + fail( + 'cab-multipart-unsupported', + 'Multi-cabinet archives are not supported for local .onepkg files.', + { offset: 30, structure: 'CFHEADER.flags' } + ); + } + if (folderCount > limits.maxFolders || fileCount > limits.maxFiles) { + fail( + 'cab-entry-count-limit', + `Cabinet contains ${folderCount} folders and ${fileCount} files, exceeding configured limits.`, + { structure: 'CFHEADER' } + ); + } + + // Re-bind reads to the trusted cbCabinet boundary before parsing variable data. + const reader = new BinaryReader(bytes, header.offset, declaredSize); + let folderReserveSize = 0; + let dataReserveSize = 0; + if ((flags & FLAG_RESERVE_PRESENT) !== 0) { + const headerReserveSize = reader.readU16('CFHEADER.cbCFHeader'); + folderReserveSize = reader.readU8('CFHEADER.cbCFFolder'); + dataReserveSize = reader.readU8('CFHEADER.cbCFData'); + reader.skip(headerReserveSize, 'CFHEADER.abReserve'); + } + + const folders: CabFolderInternal[] = []; + let totalDataBlocks = 0; + for (let index = 0; index < folderCount; index += 1) { + if ((index & 0xff) === 0) checkCancellation(options.cancellation); + const folderOffset = reader.offset; + const firstDataBlockOffset = reader.readU32('CFFOLDER.coffCabStart'); + const dataBlockCount = reader.readU16('CFFOLDER.cCFData'); + const compressionRawOffset = reader.offset; + const compression = parseCompression( + reader.readU16('CFFOLDER.typeCompress'), + limits, + compressionRawOffset + ); + reader.skip(folderReserveSize, 'CFFOLDER.abReserve'); + totalDataBlocks = checkedAdd( + totalDataBlocks, + dataBlockCount, + 'cab-data-block-count-overflow', + 'CFFOLDER.cCFData' + ); + if (totalDataBlocks > limits.maxDataBlocks) { + fail( + 'cab-data-block-count-limit', + `Cabinet exceeds the configured ${limits.maxDataBlocks}-block limit.`, + { offset: folderOffset, structure: 'CFFOLDER.cCFData' } + ); + } + folders.push({ + index, + firstDataBlockOffset, + dataBlockCount, + compression, + blocks: [], + compressedSize: 0, + uncompressedSize: 0, + dataEnd: firstDataBlockOffset, + }); + } + + if (firstFileOffset < reader.offset || firstFileOffset >= declaredSize) { + fail('cab-invalid-file-table-offset', 'CAB file table offset is invalid.', { + offset: 16, + structure: 'CFHEADER.coffFiles', + }); + } + const fileReader = new BinaryReader(bytes, firstFileOffset, declaredSize); + const entries: CabFileEntry[] = []; + const diagnostics = []; + const duplicatePaths = new Set(); + let extractedByteTotal = 0; + for (let index = 0; index < fileCount; index += 1) { + if ((index & 0xff) === 0) checkCancellation(options.cancellation); + const entryOffset = fileReader.offset; + const uncompressedSize = fileReader.readU32('CFFILE.cbFile'); + const uncompressedOffset = fileReader.readU32('CFFILE.uoffFolderStart'); + const folderIndex = fileReader.readU16('CFFILE.iFolder'); + fileReader.readU16('CFFILE.date'); + fileReader.readU16('CFFILE.time'); + const attributes = fileReader.readU16('CFFILE.attribs'); + const nameOffset = fileReader.offset; + const nameBytes = fileReader.readNullTerminatedBytes( + limits.maxNameBytes, + 'CFFILE.szName' + ); + + if (folderIndex >= 0xfffd) { + fail( + 'cab-multipart-file-unsupported', + 'CAB file spans another cabinet; multi-cabinet files are unsupported.', + { offset: entryOffset + 8, structure: 'CFFILE.iFolder' } + ); + } + const folder = folders[folderIndex]; + if (folder === undefined) { + fail( + 'cab-folder-index-out-of-range', + 'CAB file folder index is out of range.', + { + offset: entryOffset + 8, + structure: 'CFFILE.iFolder', + } + ); + } + if (uncompressedSize > limits.maxFileBytes) { + fail( + 'cab-file-size-limit', + `CAB file size ${uncompressedSize} exceeds the configured ${limits.maxFileBytes}-byte limit.`, + { offset: entryOffset, structure: 'CFFILE.cbFile' } + ); + } + extractedByteTotal = checkedAdd( + extractedByteTotal, + uncompressedSize, + 'cab-extracted-size-overflow', + 'CFFILE.cbFile' + ); + if (extractedByteTotal > limits.maxExtractedBytes) { + fail( + 'cab-extracted-size-limit', + `Extracted files exceed the configured ${limits.maxExtractedBytes}-byte limit.`, + { offset: entryOffset, structure: 'CFFILE.cbFile' } + ); + } + + const path = decodeCabFileName( + nameBytes, + (attributes & ATTRIBUTE_NAME_IS_UTF8) !== 0, + nameOffset + ); + if ( + (attributes & ATTRIBUTE_NAME_IS_UTF8) === 0 && + nameBytes.some((byte) => byte > 0x7f) + ) { + diagnostics.push( + warning( + 'cab-unmarked-utf8-name', + `Decoded unmarked non-ASCII CAB filename as strict UTF-8: ${path}`, + { offset: nameOffset, structure: 'CFFILE.szName' } + ) + ); + } + const normalized = normalizeCabPath(path, limits, nameOffset); + if (duplicatePaths.has(normalized.duplicateKey)) { + fail( + 'cab-duplicate-path', + `CAB contains a duplicate normalized path: ${normalized.normalizedPath}`, + { offset: nameOffset, structure: 'CFFILE.szName' } + ); + } + duplicatePaths.add(normalized.duplicateKey); + entries.push({ + index, + path: normalized.path, + normalizedPath: normalized.normalizedPath, + folderIndex, + uncompressedOffset, + uncompressedSize, + attributes, + compression: folder.compression, + }); + } + const metadataEnd = fileReader.offset; + + let totalFolderOutput = 0; + let totalLzxOutput = 0; + for (const folder of folders) { + if (folder.firstDataBlockOffset > declaredSize) { + fail( + 'cab-invalid-data-offset', + 'CAB folder data offset is beyond the declared cabinet.', + { + offset: folder.firstDataBlockOffset, + structure: 'CFFOLDER.coffCabStart', + } + ); + } + if ( + folder.dataBlockCount > 0 && + folder.firstDataBlockOffset < metadataEnd + ) { + fail( + 'cab-data-overlaps-metadata', + 'CAB data blocks overlap the file table.', + { + offset: folder.firstDataBlockOffset, + structure: 'CFFOLDER.coffCabStart', + } + ); + } + const blockReader = new BinaryReader( + bytes, + folder.firstDataBlockOffset, + declaredSize + ); + for ( + let blockIndex = 0; + blockIndex < folder.dataBlockCount; + blockIndex += 1 + ) { + if ((blockIndex & 0xff) === 0) checkCancellation(options.cancellation); + const headerOffset = blockReader.offset; + const checksum = blockReader.readU32('CFDATA.csum'); + const compressedSize = blockReader.readU16('CFDATA.cbData'); + const uncompressedSize = blockReader.readU16('CFDATA.cbUncomp'); + const reserve = blockReader.readBytes( + dataReserveSize, + 'CFDATA.abReserve' + ); + const dataOffset = blockReader.offset; + blockReader.skip(compressedSize, 'CFDATA.ab'); + if ( + compressedSize === 0 || + uncompressedSize === 0 || + uncompressedSize > MAX_CAB_BLOCK_OUTPUT + ) { + fail( + 'cab-invalid-data-block-size', + `Invalid CAB data block sizes ${compressedSize}/${uncompressedSize}.`, + { offset: headerOffset + 4, structure: 'CFDATA' } + ); + } + if ( + folder.compression.kind === 'none' && + compressedSize !== uncompressedSize + ) { + fail( + 'cab-uncompressed-size-mismatch', + 'Uncompressed CAB block has different compressed and output sizes.', + { offset: headerOffset + 4, structure: 'CFDATA' } + ); + } + folder.compressedSize = checkedAdd( + folder.compressedSize, + compressedSize, + 'cab-folder-size-overflow', + 'CFDATA.cbData' + ); + folder.uncompressedSize = checkedAdd( + folder.uncompressedSize, + uncompressedSize, + 'cab-folder-size-overflow', + 'CFDATA.cbUncomp' + ); + if (folder.uncompressedSize > limits.maxFolderUncompressedBytes) { + fail( + 'cab-folder-size-limit', + `CAB folder exceeds the configured ${limits.maxFolderUncompressedBytes}-byte output limit.`, + { offset: headerOffset + 6, structure: 'CFDATA.cbUncomp' } + ); + } + folder.blocks.push({ + headerOffset, + checksum, + compressedSize, + uncompressedSize, + reserve, + dataOffset, + }); + } + folder.dataEnd = blockReader.offset; + if ( + folder.uncompressedSize / folder.compressedSize > + limits.maxCompressionRatio + ) { + fail( + 'cab-compression-ratio-limit', + `CAB folder compression ratio exceeds the configured ${limits.maxCompressionRatio}:1 limit.`, + { offset: folder.firstDataBlockOffset, structure: 'CFFOLDER/CFDATA' } + ); + } + totalFolderOutput = checkedAdd( + totalFolderOutput, + folder.uncompressedSize, + 'cab-total-size-overflow', + 'CFDATA.cbUncomp' + ); + if (folder.compression.kind === 'lzx') { + totalLzxOutput = checkedAdd( + totalLzxOutput, + folder.uncompressedSize, + 'lzx-operation-limit', + 'CFDATA.cbUncomp' + ); + } + } + if (totalFolderOutput > limits.maxTotalUncompressedBytes) { + fail( + 'cab-total-size-limit', + `CAB output exceeds the configured ${limits.maxTotalUncompressedBytes}-byte limit.`, + { structure: 'CFDATA.cbUncomp' } + ); + } + if (totalLzxOutput > limits.maxDecodeOperations) { + fail( + 'lzx-operation-limit', + `LZX output exceeds the configured ${limits.maxDecodeOperations}-operation limit.`, + { structure: 'CFDATA.cbUncomp' } + ); + } + + const dataRanges = folders + .filter((folder) => folder.dataBlockCount > 0) + .map((folder) => ({ + start: folder.firstDataBlockOffset, + end: folder.dataEnd, + })) + .sort((left, right) => left.start - right.start); + for (let index = 1; index < dataRanges.length; index += 1) { + if (dataRanges[index]!.start < dataRanges[index - 1]!.end) { + fail('cab-overlapping-folders', 'CAB folder data ranges overlap.', { + offset: dataRanges[index]!.start, + structure: 'CFFOLDER.coffCabStart', + }); + } + } + + for (const entry of entries) { + const folder = folders[entry.folderIndex]!; + const fileEnd = checkedAdd( + entry.uncompressedOffset, + entry.uncompressedSize, + 'cab-file-range-overflow', + 'CFFILE' + ); + if (fileEnd > folder.uncompressedSize) { + fail( + 'cab-file-range-out-of-bounds', + `CAB file ${entry.normalizedPath} extends beyond its folder data.`, + { structure: 'CFFILE' } + ); + } + } + + if (bytes.byteLength > declaredSize) { + diagnostics.push( + warning( + 'cab-trailing-data', + `Ignored ${bytes.byteLength - declaredSize} bytes after the declared cabinet.`, + { offset: declaredSize, structure: 'CFHEADER.cbCabinet' } + ) + ); + } + if (reserved1 !== 0 || reserved2 !== 0 || reserved3 !== 0) { + diagnostics.push( + warning( + 'cab-reserved-header-data', + 'CAB header contains non-zero reserved fields.', + { structure: 'CFHEADER' } + ) + ); + } + for (const folder of folders) { + if ( + folder.compression.kind === 'mszip' || + folder.compression.kind === 'quantum' + ) { + diagnostics.push( + warning( + 'cab-compression-unsupported', + `${folder.compression.label} metadata can be listed, but this build cannot extract it.`, + { structure: `CFFOLDER[${folder.index}].typeCompress` } + ) + ); + } + } + + return { + bytes, + limits, + folders, + result: { + cabinetSetId, + cabinetSetIndex, + declaredSize, + entries, + folders: folders.map((folder) => ({ + index: folder.index, + dataBlockCount: folder.dataBlockCount, + compressedSize: folder.compressedSize, + uncompressedSize: folder.uncompressedSize, + compression: folder.compression, + })), + diagnostics, + }, + }; +} + +export function enumerateCabinet( + bytes: Uint8Array, + options: CabOpenOptions = {} +): CabEnumerationResult { + return parseCabinet(bytes, options).result; +} + +export async function extractCabinet( + bytes: Uint8Array, + options: CabOpenOptions = {} +): Promise { + const parsed = parseCabinet(bytes, options); + const byFolder = new Map(); + for (const entry of parsed.result.entries) { + const current = byFolder.get(entry.folderIndex); + if (current === undefined) byFolder.set(entry.folderIndex, [entry]); + else current.push(entry); + } + + const extractedEntries = []; + for (const folder of parsed.folders) { + checkCancellation(options.cancellation); + if ( + folder.compression.kind === 'mszip' || + folder.compression.kind === 'quantum' + ) { + fail( + 'cab-compression-unsupported', + `${folder.compression.label} extraction is not implemented.`, + { structure: `CFFOLDER[${folder.index}].typeCompress` } + ); + } + + const folderOutput = new Uint8Array(folder.uncompressedSize); + const decoder = + folder.compression.kind === 'lzx' + ? new LzxDecoder(folder.compression.windowBits, { + maxOperations: parsed.limits.maxDecodeOperations, + checkCancellation: () => checkCancellation(options.cancellation), + }) + : undefined; + let outputOffset = 0; + for (const block of folder.blocks) { + checkCancellation(options.cancellation); + const compressed = parsed.bytes.subarray( + block.dataOffset, + block.dataOffset + block.compressedSize + ); + if (block.checksum !== 0) { + const actual = calculateDataBlockChecksum( + block.reserve, + compressed, + block.compressedSize, + block.uncompressedSize + ); + if (actual !== block.checksum) { + fail( + 'cab-checksum-mismatch', + `CAB data block checksum mismatch: expected 0x${block.checksum.toString(16).padStart(8, '0')}, got 0x${actual.toString(16).padStart(8, '0')}.`, + { offset: block.headerOffset, structure: 'CFDATA.csum' } + ); + } + } + const output = + decoder === undefined + ? compressed + : decoder.decompressNext(compressed, block.uncompressedSize); + if (output.length !== block.uncompressedSize) { + fail( + 'cab-output-size-mismatch', + `CAB decoder returned ${output.length} bytes; expected ${block.uncompressedSize}.`, + { offset: block.headerOffset + 6, structure: 'CFDATA.cbUncomp' } + ); + } + folderOutput.set(output, outputOffset); + outputOffset += output.length; + await yieldForCancellation(options.cancellation); + } + decoder?.finish(); + if (outputOffset !== folderOutput.length) { + fail( + 'cab-folder-output-size-mismatch', + 'CAB folder output is incomplete.', + { + structure: `CFFOLDER[${folder.index}]`, + } + ); + } + + for (const entry of byFolder.get(folder.index) ?? []) { + const start = entry.uncompressedOffset; + extractedEntries.push({ + ...entry, + bytes: folderOutput.slice(start, start + entry.uncompressedSize), + }); + } + } + extractedEntries.sort((left, right) => left.index - right.index); + + return { ...parsed.result, extractedEntries }; +} diff --git a/src/onenote/onepkg/cancellation.ts b/src/onenote/onepkg/cancellation.ts new file mode 100644 index 0000000..13002a9 --- /dev/null +++ b/src/onenote/onepkg/cancellation.ts @@ -0,0 +1,20 @@ +import { fail } from './errors.js'; +import type { OnePkgCancellation } from './types.js'; + +export function checkCancellation( + cancellation: OnePkgCancellation | undefined +): void { + if (cancellation?.signal?.aborted || cancellation?.isCancelled?.()) { + fail('onepkg-cancelled', 'Opening the OneNote package was cancelled.', { + structure: 'onepkg', + }); + } +} + +export async function yieldForCancellation( + cancellation: OnePkgCancellation | undefined +): Promise { + checkCancellation(cancellation); + await cancellation?.yield?.(); + checkCancellation(cancellation); +} diff --git a/src/onenote/onepkg/checksum.ts b/src/onenote/onepkg/checksum.ts new file mode 100644 index 0000000..32fa2e0 --- /dev/null +++ b/src/onenote/onepkg/checksum.ts @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: MIT + * + * Direct TypeScript translation of rust-cab's checksum state machine. + * rust-cab commit c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2 + * Copyright (c) 2017 Matthew D. Steele, MIT License. + * Modified for bounded Uint8Array input and CAB CFDATA verification. + */ + +export class CabChecksum { + private checksum = 0; + private remainder = 0; + private remainderShift = 0; + + update(bytes: Uint8Array): void { + for (const byte of bytes) { + this.remainder = (this.remainder | (byte << this.remainderShift)) >>> 0; + if (this.remainderShift === 24) { + this.checksum = (this.checksum ^ this.remainder) >>> 0; + this.remainder = 0; + this.remainderShift = 0; + } else { + this.remainderShift += 8; + } + } + } + + value(): number { + switch (this.remainderShift) { + case 0: + return this.checksum >>> 0; + case 8: + return (this.checksum ^ this.remainder) >>> 0; + case 16: + return ( + (this.checksum ^ + (this.remainder >>> 8) ^ + ((this.remainder & 0xff) << 8)) >>> + 0 + ); + case 24: + return ( + (this.checksum ^ + (this.remainder >>> 16) ^ + (this.remainder & 0xff00) ^ + ((this.remainder & 0xff) << 16)) >>> + 0 + ); + default: + throw new Error('Invalid CAB checksum state'); + } + } +} + +export function calculateDataBlockChecksum( + reserve: Uint8Array, + compressed: Uint8Array, + compressedSize: number, + uncompressedSize: number +): number { + const checksum = new CabChecksum(); + checksum.update(reserve); + checksum.update(compressed); + return ( + (checksum.value() ^ ((compressedSize | (uncompressedSize << 16)) >>> 0)) >>> + 0 + ); +} diff --git a/src/onenote/onepkg/errors.ts b/src/onenote/onepkg/errors.ts new file mode 100644 index 0000000..cf008be --- /dev/null +++ b/src/onenote/onepkg/errors.ts @@ -0,0 +1,64 @@ +import type { ParserDiagnostic } from '../model/diagnostics.js'; + +export class OnePkgError extends Error { + readonly diagnostic: ParserDiagnostic; + + constructor(diagnostic: ParserDiagnostic, options?: ErrorOptions) { + super(diagnostic.message, options); + this.name = 'OnePkgError'; + this.diagnostic = diagnostic; + } +} + +export interface DiagnosticLocation { + offset?: number; + structure?: string; +} + +export function fail( + code: string, + message: string, + location: DiagnosticLocation = {} +): never { + throw new OnePkgError({ + severity: 'error', + code, + message, + ...location, + recoverable: false, + }); +} + +export function warning( + code: string, + message: string, + location: DiagnosticLocation = {} +): ParserDiagnostic { + return { + severity: 'warning', + code, + message, + ...location, + recoverable: true, + }; +} + +export function diagnosticFromUnknown( + error: unknown, + code: string, + message: string, + structure?: string +): ParserDiagnostic { + if (error instanceof OnePkgError) { + return error.diagnostic; + } + + const detail = error instanceof Error ? `: ${error.message}` : ''; + return { + severity: 'error', + code, + message: `${message}${detail}`, + structure, + recoverable: true, + }; +} diff --git a/src/onenote/onepkg/index.ts b/src/onenote/onepkg/index.ts new file mode 100644 index 0000000..cf9a17d --- /dev/null +++ b/src/onenote/onepkg/index.ts @@ -0,0 +1,23 @@ +export { enumerateCabinet, extractCabinet } from './cabinet.js'; +export { OnePkgError } from './errors.js'; +export { openOneNotePackage } from './package.js'; +export type { + OneNotePackageOpenOptions, + OneNotePackageOpenResult, + OneNotePackageSectionParseOutcome, + OneNotePackageSectionParseRequest, + OneNotePackageSectionParser, + ParsedOneNotePackageSection, +} from './package.js'; +export { + DEFAULT_ONEPKG_LIMITS, + type CabCompressionMethod, + type CabEnumerationResult, + type CabExtractionResult, + type CabFileEntry, + type CabFolderSummary, + type CabOpenOptions, + type ExtractedCabFile, + type OnePkgCancellation, + type OnePkgLimits, +} from './types.js'; diff --git a/src/onenote/onepkg/joplin-fixture.test.ts b/src/onenote/onepkg/joplin-fixture.test.ts new file mode 100644 index 0000000..9c67f2a --- /dev/null +++ b/src/onenote/onepkg/joplin-fixture.test.ts @@ -0,0 +1,114 @@ +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { parseOneNoteSection } from '../parser/parse-section.js'; +import { enumerateCabinet, extractCabinet } from './cabinet.js'; +import { openOneNotePackage } from './package.js'; + +/* + * Joplin CLI test fixture, AGPL-3.0-or-later. See tests/fixtures/README.md. + * Pinned upstream revision: 1e73aad7eb08fde5d9e4cb533df40052a5cd32d7 + */ +const encodedFixture = readFileSync( + resolve(process.cwd(), 'tests/fixtures/joplin-test.onepkg.base64'), + 'utf8' +); +const JOPLIN_ONEPKG = Uint8Array.from( + Buffer.from(encodedFixture.replaceAll(/\s/gu, ''), 'base64') +); + +const ONE_MAGIC = Uint8Array.from([ + 0xe4, 0x52, 0x5c, 0x7b, 0x8c, 0xd8, 0xa7, 0x4d, 0xae, 0xb1, 0x53, 0x78, 0xd0, + 0x29, 0x96, 0xd3, +]); + +const EXPECTED_PATHS = [ + 'Another section.one', + 'Open Notebook.onetoc2', + 'Section group/A.one', + 'Section group/B.one', + 'Section group/Open Notebook.onetoc2', + 'Tést!.one', + '⅀⸨ Unicode ⸩.one', +]; + +describe('pinned Joplin .onepkg fixture', () => { + it('retains the reviewed upstream bytes', () => { + expect(JOPLIN_ONEPKG).toHaveLength(11_066); + expect(createHash('sha256').update(JOPLIN_ONEPKG).digest('hex')).toBe( + '83cb622d40f0ab127038b629553b6926be3e2a6ce642f169df0e2614c13e8469' + ); + }); + + it('enumerates all seven UTF-8/nested entries using LZX 0x1203', () => { + const listing = enumerateCabinet(JOPLIN_ONEPKG); + expect(listing.entries.map((entry) => entry.normalizedPath)).toEqual( + EXPECTED_PATHS + ); + expect(listing.folders).toHaveLength(1); + expect(listing.folders[0]).toMatchObject({ + dataBlockCount: 3, + compression: { kind: 'lzx', raw: 0x1203, windowBits: 18 }, + }); + expect( + listing.diagnostics.filter( + (diagnostic) => diagnostic.code === 'cab-unmarked-utf8-name' + ) + ).toHaveLength(2); + }); + + it('extracts every entry and preserves native .one headers', async () => { + const extraction = await extractCabinet(JOPLIN_ONEPKG); + expect( + extraction.extractedEntries.map((entry) => entry.normalizedPath) + ).toEqual(EXPECTED_PATHS); + const sections = extraction.extractedEntries.filter((entry) => + entry.normalizedPath.toLowerCase().endsWith('.one') + ); + expect(sections).toHaveLength(5); + for (const section of sections) { + expect(section.bytes.subarray(0, ONE_MAGIC.length)).toEqual(ONE_MAGIC); + } + + const opened = await openOneNotePackage(JOPLIN_ONEPKG, { + sourceName: 'test.onepkg', + }); + expect(opened.package.entries).toHaveLength(7); + expect(opened.package.sections).toHaveLength(5); + expect( + opened.package.sections.every( + (section) => section.parseStatus === 'not-requested' + ) + ).toBe(true); + }); + + it('opens and parses all five packaged sections end to end', async () => { + const opened = await openOneNotePackage(JOPLIN_ONEPKG, { + sourceName: 'test.onepkg', + parseSection: ({ bytes, normalizedPath }) => ({ + status: 'parsed', + value: parseOneNoteSection(bytes, { sourceName: normalizedPath }), + }), + }); + + expect(opened.package.sections).toHaveLength(5); + expect( + opened.package.sections.map((section) => section.parseStatus) + ).toEqual(['parsed', 'parsed', 'parsed', 'parsed', 'parsed']); + expect( + opened.package.entries.some((entry) => entry.parseStatus === 'failed') + ).toBe(false); + + const testSection = opened.package.sections.find( + (section) => section.path === 'Tést!.one' + ); + expect(testSection?.section?.sectionName).toBe('Tést!'); + expect(testSection?.section?.pages[0]).toMatchObject({ + title: 'Testing…', + text: 'Link to page: Page 2', + }); + }); +}); diff --git a/src/onenote/onepkg/lzx/bit-reader.ts b/src/onenote/onepkg/lzx/bit-reader.ts new file mode 100644 index 0000000..8ace0b3 --- /dev/null +++ b/src/onenote/onepkg/lzx/bit-reader.ts @@ -0,0 +1,141 @@ +/* + * SPDX-License-Identifier: MIT + * + * Direct TypeScript port of lzxd 0.2.5 `bitstream.rs`. + * lzxd commit 4748e43594e3e30cff2ace3a6ad7a376c9816fdd + * Copyright (c) 2020 Lonami, MIT OR Apache-2.0 (MIT selected here). + * Modified to use checked browser-native Uint8Array reads. + */ + +import { fail } from '../errors.js'; + +export class LzxBitReader { + private byteOffset = 0; + private word = 0; + private bitsRemaining = 0; + + constructor(private readonly bytes: Uint8Array) {} + + get offset(): number { + return this.byteOffset; + } + + get remainingRawBytes(): number { + return this.bytes.length - this.byteOffset; + } + + private loadWord(): void { + if (this.byteOffset > this.bytes.length - 2) { + fail('lzx-unexpected-eof', 'Unexpected end of LZX chunk.', { + offset: this.byteOffset, + structure: 'LZX bitstream', + }); + } + this.word = + this.bytes[this.byteOffset]! | (this.bytes[this.byteOffset + 1]! << 8); + this.byteOffset += 2; + this.bitsRemaining = 16; + } + + readBits(bitCount: number): number { + if (!Number.isInteger(bitCount) || bitCount < 0 || bitCount > 32) { + throw new RangeError('LZX bit reads must contain between 0 and 32 bits'); + } + + let result = 0; + let remaining = bitCount; + while (remaining > 0) { + if (this.bitsRemaining === 0) { + this.loadWord(); + } + const take = Math.min(remaining, this.bitsRemaining); + const shift = this.bitsRemaining - take; + const part = (this.word >>> shift) & (2 ** take - 1); + result = result * 2 ** take + part; + this.bitsRemaining -= take; + remaining -= take; + } + return result; + } + + readBit(): number { + return this.readBits(1); + } + + /** + * Huffman lookup may look beyond the end of a chunk. The reference decoder + * pads that look-ahead with zeroes; consuming the actual code remains strict. + */ + peekBits(bitCount: number): number { + if (!Number.isInteger(bitCount) || bitCount < 0 || bitCount > 32) { + throw new RangeError('LZX bit peeks must contain between 0 and 32 bits'); + } + + let offset = this.byteOffset; + let word = this.word; + let wordRemaining = this.bitsRemaining; + let result = 0; + let remaining = bitCount; + while (remaining > 0) { + if (wordRemaining === 0) { + if (offset > this.bytes.length - 2) { + word = 0; + } else { + word = this.bytes[offset]! | (this.bytes[offset + 1]! << 8); + offset += 2; + } + wordRemaining = 16; + } + const take = Math.min(remaining, wordRemaining); + const shift = wordRemaining - take; + result = result * 2 ** take + ((word >>> shift) & (2 ** take - 1)); + wordRemaining -= take; + remaining -= take; + } + return result; + } + + readU32LittleEndian(): number { + const low = this.readBits(16); + const high = this.readBits(16); + return (low + high * 0x1_0000) >>> 0; + } + + readU24BigEndianBits(): number { + return this.readBits(16) * 0x100 + this.readBits(8); + } + + /** Align exactly as lzxd 0.2.5 does before an uncompressed block. */ + alignToWord(): void { + if (this.bitsRemaining === 0) { + this.readBits(16); + } else { + this.bitsRemaining = 0; + } + } + + readRawByte(): number { + if (this.byteOffset >= this.bytes.length) { + fail('lzx-unexpected-eof', 'Missing LZX uncompressed-block padding.', { + offset: this.byteOffset, + structure: 'LZX uncompressed block', + }); + } + return this.bytes[this.byteOffset++]!; + } + + readRaw(length: number): Uint8Array { + if (length < 0 || this.byteOffset > this.bytes.length - length) { + fail('lzx-unexpected-eof', 'Unexpected end of LZX raw block.', { + offset: this.byteOffset, + structure: 'LZX uncompressed block', + }); + } + const value = this.bytes.subarray( + this.byteOffset, + this.byteOffset + length + ); + this.byteOffset += length; + return value; + } +} diff --git a/src/onenote/onepkg/lzx/decoder.ts b/src/onenote/onepkg/lzx/decoder.ts new file mode 100644 index 0000000..accc94c --- /dev/null +++ b/src/onenote/onepkg/lzx/decoder.ts @@ -0,0 +1,430 @@ +/* + * SPDX-License-Identifier: MIT + * + * Direct TypeScript port of lzxd 0.2.5 `lib.rs`, `block.rs`, and `window.rs`. + * lzxd commit 4748e43594e3e30cff2ace3a6ad7a376c9816fdd + * Copyright (c) 2020 Lonami, MIT OR Apache-2.0 (MIT selected here). + * Modified for CAB chunks, bounded allocation/operations, cancellation, and + * strict errors instead of assertions. + */ + +import { fail } from '../errors.js'; +import { LzxBitReader } from './bit-reader.js'; +import { LzxCanonicalTree, LzxHuffmanTree } from './huffman.js'; + +const MAX_CHUNK_SIZE = 32 * 1024; +const POSITION_SLOTS: Readonly> = { + 15: 30, + 16: 32, + 17: 34, + 18: 36, + 19: 38, + 20: 42, + 21: 50, + 22: 66, + 23: 98, + 24: 162, + 25: 290, +}; + +interface VerbatimBlock { + kind: 'verbatim'; + size: number; + remaining: number; + mainTree: LzxHuffmanTree; + lengthTree?: LzxHuffmanTree; +} + +interface AlignedBlock { + kind: 'aligned'; + size: number; + remaining: number; + alignedTree: LzxHuffmanTree; + mainTree: LzxHuffmanTree; + lengthTree?: LzxHuffmanTree; +} + +interface UncompressedBlock { + kind: 'uncompressed'; + size: number; + remaining: number; +} + +type LzxBlock = VerbatimBlock | AlignedBlock | UncompressedBlock; + +export interface LzxDecoderOptions { + maxOperations: number; + checkCancellation?: () => void; +} + +function footerBits(positionSlot: number): number { + if (positionSlot < 4) return 0; + if (positionSlot >= 36) return 17; + return Math.floor((positionSlot - 2) / 2); +} + +function basePosition(positionSlot: number): number { + if (positionSlot < 4) return positionSlot; + if (positionSlot >= 36) return (positionSlot - 34) * 0x2_0000; + return (2 + (positionSlot & 1)) * 2 ** footerBits(positionSlot); +} + +export class LzxDecoder { + private readonly window: Uint8Array; + private readonly windowMask: number; + private windowPosition = 0; + private readonly mainTree: LzxCanonicalTree; + private readonly lengthTree = new LzxCanonicalTree(249); + private readonly positionSlotCount: number; + private repeatedOffsets = [1, 1, 1]; + private firstChunkRead = false; + private currentBlock?: LzxBlock; + private e8TranslationSize?: number; + private totalOutput = 0; + private operations = 0; + private nextCancellationCheck = 4_096; + + constructor( + windowBits: number, + private readonly options: LzxDecoderOptions + ) { + const positionSlotCount = POSITION_SLOTS[windowBits]; + if (positionSlotCount === undefined) { + fail( + 'lzx-invalid-window', + `Unsupported LZX window size 2^${windowBits}.`, + { + structure: 'CFFOLDER.typeCompress', + } + ); + } + const windowSize = 2 ** windowBits; + this.window = new Uint8Array(windowSize); + this.windowMask = windowSize - 1; + this.positionSlotCount = positionSlotCount; + this.mainTree = new LzxCanonicalTree(256 + 8 * positionSlotCount); + } + + decompressNext(chunk: Uint8Array, outputLength: number): Uint8Array { + if ( + !Number.isInteger(outputLength) || + outputLength <= 0 || + outputLength > MAX_CHUNK_SIZE + ) { + fail( + 'lzx-invalid-chunk-size', + `LZX CAB chunk output size ${outputLength} is outside 1..${MAX_CHUNK_SIZE}.`, + { structure: 'CFDATA.cbUncomp' } + ); + } + + const reader = new LzxBitReader(chunk); + if (!this.firstChunkRead) { + this.firstChunkRead = true; + if (reader.readBit() !== 0) { + this.e8TranslationSize = reader.readBits(32) | 0; + } + } + + const output = new Uint8Array(outputLength); + let outputOffset = 0; + while (outputOffset < output.length) { + this.checkWork(); + if ( + this.currentBlock === undefined || + this.currentBlock.remaining === 0 + ) { + if ( + this.currentBlock?.kind === 'uncompressed' && + this.currentBlock.size % 2 !== 0 + ) { + reader.readRawByte(); + } + this.currentBlock = this.readBlock(reader); + } + + const block = this.currentBlock; + if (block.kind === 'uncompressed') { + // An LZX uncompressed block may continue in the next CAB CFDATA + // chunk. Consume only the raw bytes present in this compressed chunk. + const length = Math.min( + block.remaining, + output.length - outputOffset, + reader.remainingRawBytes + ); + if (length === 0) { + fail( + 'lzx-unexpected-eof', + 'LZX chunk ended before producing its declared CAB output size.', + { offset: reader.offset, structure: 'LZX uncompressed block' } + ); + } + const raw = reader.readRaw(length); + for (let index = 0; index < raw.length; index += 1) { + this.writeByte(raw[index]!, output, outputOffset + index); + } + outputOffset += length; + this.advanceBlock(block, length); + continue; + } + + const mainElement = block.mainTree.decode(reader); + if (mainElement < 256) { + this.ensureAdvance(block, output, outputOffset, 1); + this.writeByte(mainElement, output, outputOffset); + outputOffset += 1; + this.advanceBlock(block, 1); + continue; + } + + const lengthHeader = (mainElement - 256) & 7; + const matchLength = + lengthHeader === 7 + ? (block.lengthTree ?? this.missingLengthTree()).decode(reader) + 9 + : lengthHeader + 2; + const positionSlot = (mainElement - 256) >>> 3; + if (positionSlot >= this.positionSlotCount) { + fail( + 'lzx-invalid-position-slot', + 'LZX match position is out of range.', + { + offset: reader.offset, + structure: 'LZX token', + } + ); + } + const matchOffset = this.decodeMatchOffset( + reader, + positionSlot, + block.kind === 'aligned' ? block.alignedTree : undefined + ); + this.ensureAdvance(block, output, outputOffset, matchLength); + if ( + matchOffset <= 0 || + matchOffset > this.window.length || + matchOffset > this.totalOutput + outputOffset + ) { + fail( + 'lzx-invalid-match-offset', + `LZX match offset ${matchOffset} is outside the populated window.`, + { offset: reader.offset, structure: 'LZX token' } + ); + } + for (let index = 0; index < matchLength; index += 1) { + const value = + this.window[ + (this.windowPosition - matchOffset + this.window.length) & + this.windowMask + ]!; + this.writeByte(value, output, outputOffset + index); + } + outputOffset += matchLength; + this.advanceBlock(block, matchLength); + } + + const chunkOffset = this.totalOutput; + this.totalOutput += output.length; + if ( + this.e8TranslationSize !== undefined && + chunkOffset < 0x4000_0000 && + output.length > 10 + ) { + this.postprocessE8(output, chunkOffset, this.e8TranslationSize); + } + return output; + } + + finish(): void { + if (this.currentBlock !== undefined && this.currentBlock.remaining !== 0) { + fail( + 'lzx-incomplete-block', + `LZX stream ended with ${this.currentBlock.remaining} uncompressed block bytes missing.`, + { structure: 'LZX block' } + ); + } + } + + private readBlock(reader: LzxBitReader): LzxBlock { + const kind = reader.readBits(3); + const size = reader.readU24BigEndianBits(); + if (size === 0) { + fail('lzx-invalid-block-size', 'LZX block size cannot be zero.', { + offset: reader.offset, + structure: 'LZX block header', + }); + } + + if (kind === 1 || kind === 2) { + let alignedTree: LzxHuffmanTree | undefined; + if (kind === 2) { + const alignedLengths = new Uint8Array(8); + for (let index = 0; index < alignedLengths.length; index += 1) { + alignedLengths[index] = reader.readBits(3); + } + alignedTree = LzxHuffmanTree.fromPathLengths(alignedLengths); + } + this.mainTree.updateWithPretree(reader, 0, 256); + this.mainTree.updateWithPretree( + reader, + 256, + 256 + 8 * this.positionSlotCount + ); + this.lengthTree.updateWithPretree(reader, 0, 249); + const mainTree = this.mainTree.create()!; + const lengthTree = this.lengthTree.create(true); + if (kind === 2) { + return { + kind: 'aligned', + size, + remaining: size, + alignedTree: alignedTree!, + mainTree, + lengthTree, + }; + } + return { kind: 'verbatim', size, remaining: size, mainTree, lengthTree }; + } + + if (kind === 3) { + reader.alignToWord(); + this.repeatedOffsets = [ + reader.readU32LittleEndian(), + reader.readU32LittleEndian(), + reader.readU32LittleEndian(), + ]; + return { kind: 'uncompressed', size, remaining: size }; + } + + fail('lzx-invalid-block-type', `Invalid LZX block type ${kind}.`, { + offset: reader.offset, + structure: 'LZX block header', + }); + } + + private decodeMatchOffset( + reader: LzxBitReader, + positionSlot: number, + alignedTree: LzxHuffmanTree | undefined + ): number { + if (positionSlot === 0) return this.repeatedOffsets[0]!; + if (positionSlot === 1) { + const offset = this.repeatedOffsets[1]!; + [this.repeatedOffsets[0], this.repeatedOffsets[1]] = [ + this.repeatedOffsets[1]!, + this.repeatedOffsets[0]!, + ]; + return offset; + } + if (positionSlot === 2) { + const offset = this.repeatedOffsets[2]!; + [this.repeatedOffsets[0], this.repeatedOffsets[2]] = [ + this.repeatedOffsets[2]!, + this.repeatedOffsets[0]!, + ]; + return offset; + } + + const bitCount = footerBits(positionSlot); + let formattedOffset: number; + if (alignedTree !== undefined && bitCount >= 3) { + formattedOffset = + basePosition(positionSlot) + + reader.readBits(bitCount - 3) * 8 + + alignedTree.decode(reader); + } else { + formattedOffset = basePosition(positionSlot) + reader.readBits(bitCount); + } + const offset = formattedOffset - 2; + this.repeatedOffsets[2] = this.repeatedOffsets[1]!; + this.repeatedOffsets[1] = this.repeatedOffsets[0]!; + this.repeatedOffsets[0] = offset; + return offset; + } + + private missingLengthTree(): never { + fail( + 'lzx-empty-length-tree', + 'LZX token needs a length footer but the length tree is empty.', + { structure: 'LZX length tree' } + ); + } + + private ensureAdvance( + block: LzxBlock, + output: Uint8Array, + outputOffset: number, + length: number + ): void { + if (length <= 0 || length > block.remaining) { + fail('lzx-block-overread', 'LZX token exceeds its declared block size.', { + structure: 'LZX block', + }); + } + if (length > output.length - outputOffset) { + fail( + 'lzx-chunk-overread', + 'LZX token crosses the declared CAB chunk output boundary.', + { structure: 'CFDATA.cbUncomp' } + ); + } + } + + private advanceBlock(block: LzxBlock, length: number): void { + if (length <= 0 || length > block.remaining) { + fail('lzx-block-overread', 'LZX data exceeds its declared block size.', { + structure: 'LZX block', + }); + } + block.remaining -= length; + this.operations += length; + } + + private writeByte( + value: number, + output: Uint8Array, + outputOffset: number + ): void { + this.window[this.windowPosition] = value; + this.windowPosition = (this.windowPosition + 1) & this.windowMask; + output[outputOffset] = value; + } + + private checkWork(): void { + if (this.operations > this.options.maxOperations) { + fail( + 'lzx-operation-limit', + `LZX decoding exceeded the configured ${this.options.maxOperations}-operation limit.`, + { structure: 'LZX stream' } + ); + } + if (this.operations >= this.nextCancellationCheck) { + this.options.checkCancellation?.(); + this.nextCancellationCheck = this.operations + 4_096; + } + } + + private postprocessE8( + data: Uint8Array, + chunkOffset: number, + translationSize: number + ): void { + const view = new DataView(data.buffer, data.byteOffset, data.byteLength); + let processed = 0; + while (processed < data.length) { + const relative = data.subarray(processed).indexOf(0xe8); + if (relative < 0) return; + const position = processed + relative; + if (data.length - position <= 10) return; + const currentPointer = chunkOffset + position; + const absoluteValue = view.getInt32(position + 1, true); + if (absoluteValue >= -currentPointer && absoluteValue < translationSize) { + const relativeValue = + absoluteValue > 0 + ? absoluteValue - currentPointer + : absoluteValue + translationSize; + view.setInt32(position + 1, relativeValue, true); + } + processed = position + 5; + } + } +} diff --git a/src/onenote/onepkg/lzx/huffman.ts b/src/onenote/onepkg/lzx/huffman.ts new file mode 100644 index 0000000..26d8a45 --- /dev/null +++ b/src/onenote/onepkg/lzx/huffman.ts @@ -0,0 +1,161 @@ +/* + * SPDX-License-Identifier: MIT + * + * Direct TypeScript port of lzxd 0.2.5 `tree.rs`. + * lzxd commit 4748e43594e3e30cff2ace3a6ad7a376c9816fdd + * Copyright (c) 2020 Lonami, MIT OR Apache-2.0 (MIT selected here). + * Modified to use typed arrays and explicit malformed-tree diagnostics. + */ + +import { fail } from '../errors.js'; +import { LzxBitReader } from './bit-reader.js'; + +export class LzxCanonicalTree { + private readonly pathLengths: Uint8Array; + + constructor(elementCount: number) { + this.pathLengths = new Uint8Array(elementCount); + } + + create(allowEmpty = false): LzxHuffmanTree | undefined { + let largestLength = 0; + for (const length of this.pathLengths) { + largestLength = Math.max(largestLength, length); + } + if (largestLength === 0) { + if (allowEmpty) return undefined; + fail('lzx-empty-tree', 'LZX stream requires a non-empty Huffman tree.', { + structure: 'LZX Huffman tree', + }); + } + return LzxHuffmanTree.fromPathLengths(this.pathLengths, largestLength); + } + + updateWithPretree(reader: LzxBitReader, start: number, end: number): void { + if (start < 0 || end < start || end > this.pathLengths.length) { + throw new RangeError('Invalid LZX tree update range'); + } + + const pretreeLengths = new Uint8Array(20); + for (let index = 0; index < pretreeLengths.length; index += 1) { + pretreeLengths[index] = reader.readBits(4); + } + const pretree = LzxHuffmanTree.fromPathLengths(pretreeLengths); + + let index = start; + while (index < end) { + const code = pretree.decode(reader); + if (code <= 16) { + this.pathLengths[index] = (17 + this.pathLengths[index]! - code) % 17; + index += 1; + continue; + } + + if (code === 17 || code === 18) { + const run = + code === 17 ? reader.readBits(4) + 4 : reader.readBits(5) + 20; + if (index + run > end) { + fail( + 'lzx-invalid-pretree-rle', + 'LZX zero path-length run exceeds its tree range.', + { offset: reader.offset, structure: 'LZX pretree' } + ); + } + this.pathLengths.fill(0, index, index + run); + index += run; + continue; + } + + if (code === 19) { + const run = reader.readBits(1) + 4; + const delta = pretree.decode(reader); + if (delta > 16 || index + run > end) { + fail( + 'lzx-invalid-pretree-rle', + 'LZX repeated path-length run is invalid.', + { offset: reader.offset, structure: 'LZX pretree' } + ); + } + const value = (17 + this.pathLengths[index]! - delta) % 17; + this.pathLengths.fill(value, index, index + run); + index += run; + continue; + } + + fail('lzx-invalid-pretree-element', `Invalid LZX pretree code ${code}.`, { + offset: reader.offset, + structure: 'LZX pretree', + }); + } + } +} + +export class LzxHuffmanTree { + private constructor( + private readonly pathLengths: Uint8Array, + private readonly largestLength: number, + private readonly decodeTable: Uint16Array + ) {} + + static fromPathLengths( + source: Uint8Array, + knownLargestLength?: number + ): LzxHuffmanTree { + const pathLengths = source.slice(); + let largestLength = knownLargestLength ?? 0; + if (knownLargestLength === undefined) { + for (const length of pathLengths) { + largestLength = Math.max(largestLength, length); + } + } + if (largestLength === 0 || largestLength > 16) { + fail( + largestLength === 0 ? 'lzx-empty-tree' : 'lzx-invalid-path-lengths', + largestLength === 0 + ? 'LZX stream contains an empty Huffman tree.' + : `LZX Huffman path length ${largestLength} is invalid.`, + { structure: 'LZX Huffman tree' } + ); + } + + const decodeTable = new Uint16Array(2 ** largestLength); + let position = 0; + for (let bitLength = 1; bitLength <= largestLength; bitLength += 1) { + const amount = 2 ** (largestLength - bitLength); + for (let symbol = 0; symbol < pathLengths.length; symbol += 1) { + if (pathLengths[symbol] !== bitLength) continue; + if (position > decodeTable.length - amount) { + fail( + 'lzx-invalid-path-lengths', + 'LZX Huffman path lengths are over-subscribed.', + { structure: 'LZX Huffman tree' } + ); + } + decodeTable.fill(symbol, position, position + amount); + position += amount; + } + } + if (position !== decodeTable.length) { + fail( + 'lzx-invalid-path-lengths', + 'LZX Huffman path lengths do not form a complete tree.', + { structure: 'LZX Huffman tree' } + ); + } + + return new LzxHuffmanTree(pathLengths, largestLength, decodeTable); + } + + decode(reader: LzxBitReader): number { + const symbol = this.decodeTable[reader.peekBits(this.largestLength)]!; + const bitLength = this.pathLengths[symbol]!; + if (bitLength === 0) { + fail('lzx-invalid-huffman-code', 'Invalid LZX Huffman code.', { + offset: reader.offset, + structure: 'LZX Huffman tree', + }); + } + reader.readBits(bitLength); + return symbol; + } +} diff --git a/src/onenote/onepkg/package.ts b/src/onenote/onepkg/package.ts new file mode 100644 index 0000000..3fc9bf1 --- /dev/null +++ b/src/onenote/onepkg/package.ts @@ -0,0 +1,177 @@ +import type { + OneNotePackageDto, + OneNotePackageEntryDto, + OneNotePackagedSectionDto, + OneNoteSectionDto, +} from '../model/dto.js'; +import type { ParserDiagnostic } from '../model/diagnostics.js'; +import { checkCancellation } from './cancellation.js'; +import { extractCabinet } from './cabinet.js'; +import { diagnosticFromUnknown, OnePkgError, warning } from './errors.js'; +import type { + CabOpenOptions, + ExtractedCabFile, + OnePkgCancellation, +} from './types.js'; + +export interface OneNotePackageSectionParseRequest { + sourceName: string; + path: string; + normalizedPath: string; + bytes: Uint8Array; + cancellation?: OnePkgCancellation; +} + +export type OneNotePackageSectionParseOutcome = + | { + status: 'parsed'; + value: OneNoteSectionDto; + diagnostics?: ParserDiagnostic[]; + } + | { status: 'unsupported'; diagnostics?: ParserDiagnostic[] }; + +/** + * Deliberately independent from the CAB implementation: a `.one` parser is + * injected and receives only one extracted section plus cancellation context. + */ +export type OneNotePackageSectionParser = ( + request: OneNotePackageSectionParseRequest +) => + | OneNotePackageSectionParseOutcome + | Promise; + +export interface OneNotePackageOpenOptions extends CabOpenOptions { + sourceName?: string; + parseSection?: OneNotePackageSectionParser; +} + +export interface ParsedOneNotePackageSection { + normalizedPath: string; + value: OneNoteSectionDto; +} + +export interface OneNotePackageOpenResult { + package: OneNotePackageDto; + /** Extracted in memory only. CAB paths are never written to the filesystem. */ + extractedEntries: ExtractedCabFile[]; + parsedSections: ParsedOneNotePackageSection[]; +} + +function entryKind(path: string): OneNotePackageEntryDto['kind'] { + const lower = path.toLowerCase(); + if (lower.endsWith('.one')) return 'section'; + if (lower.endsWith('.onetoc2')) return 'table-of-contents'; + return 'other'; +} + +function compressionLabel(entry: ExtractedCabFile): string { + return entry.compression.kind === 'lzx' + ? `${entry.compression.label} 2^${entry.compression.windowBits}` + : entry.compression.label; +} + +function sectionDisplayName(path: string): string { + const fileName = path.slice(path.lastIndexOf('/') + 1); + return fileName.toLowerCase().endsWith('.one') + ? fileName.slice(0, -4) + : fileName; +} + +export async function openOneNotePackage( + bytes: Uint8Array, + options: OneNotePackageOpenOptions = {} +): Promise { + const sourceName = options.sourceName ?? 'notebook.onepkg'; + const extraction = await extractCabinet(bytes, options); + const diagnostics = [...extraction.diagnostics]; + const packageEntries: OneNotePackageEntryDto[] = + extraction.extractedEntries.map((entry) => ({ + path: entry.path, + normalizedPath: entry.normalizedPath, + kind: entryKind(entry.normalizedPath), + uncompressedSize: entry.uncompressedSize, + compressionMethod: compressionLabel(entry), + parseStatus: 'not-requested', + })); + const parsedSections: ParsedOneNotePackageSection[] = []; + const packagedSections: OneNotePackagedSectionDto[] = []; + + for (let index = 0; index < extraction.extractedEntries.length; index += 1) { + const entry = extraction.extractedEntries[index]!; + const packageEntry = packageEntries[index]!; + if (packageEntry.kind !== 'section') continue; + + const sectionDiagnostics: ParserDiagnostic[] = []; + let parsedSection: OneNoteSectionDto | undefined; + if (options.parseSection !== undefined) { + checkCancellation(options.cancellation); + try { + const outcome = await options.parseSection({ + sourceName: entry.normalizedPath, + path: entry.path, + normalizedPath: entry.normalizedPath, + bytes: entry.bytes, + cancellation: options.cancellation, + }); + if (outcome.status === 'parsed') { + packageEntry.parseStatus = 'parsed'; + parsedSection = outcome.value; + parsedSections.push({ + normalizedPath: entry.normalizedPath, + value: outcome.value, + }); + } else { + packageEntry.parseStatus = 'unsupported'; + if (outcome.diagnostics === undefined) { + sectionDiagnostics.push( + warning( + 'one-section-unsupported', + `Section parser does not support ${entry.normalizedPath}.`, + { structure: entry.normalizedPath } + ) + ); + } + } + sectionDiagnostics.push(...(outcome.diagnostics ?? [])); + } catch (error) { + if ( + error instanceof OnePkgError && + error.diagnostic.code === 'onepkg-cancelled' + ) { + throw error; + } + packageEntry.parseStatus = 'failed'; + sectionDiagnostics.push({ + ...diagnosticFromUnknown( + error, + 'one-section-parse-failed', + `Could not parse ${entry.normalizedPath}`, + entry.normalizedPath + ), + recoverable: true, + }); + } + } + packagedSections.push({ + id: entry.normalizedPath, + path: entry.normalizedPath, + displayName: sectionDisplayName(entry.normalizedPath), + parseStatus: packageEntry.parseStatus, + section: parsedSection, + diagnostics: sectionDiagnostics, + }); + } + + return { + package: { + format: 'onepkg', + sourceName, + sourceSize: bytes.byteLength, + entries: packageEntries, + sections: packagedSections, + diagnostics, + }, + extractedEntries: extraction.extractedEntries, + parsedSections, + }; +} diff --git a/src/onenote/onepkg/path.ts b/src/onenote/onepkg/path.ts new file mode 100644 index 0000000..ace8ebe --- /dev/null +++ b/src/onenote/onepkg/path.ts @@ -0,0 +1,99 @@ +import { fail } from './errors.js'; +import type { OnePkgLimits } from './types.js'; + +const STRICT_UTF8 = new TextDecoder('utf-8', { fatal: true }); + +export function decodeCabFileName( + bytes: Uint8Array, + utf8: boolean, + offset: number +): string { + try { + // OneNote/Joplin exports are observed to store UTF-8 names without setting + // ATTR_NAME_IS_UTF. Strict UTF-8 is the only deterministic compatibility + // fallback: malformed input is rejected and no legacy code page is guessed. + return STRICT_UTF8.decode(bytes); + } catch (error) { + fail( + utf8 ? 'cab-name-invalid-utf8' : 'cab-name-encoding-unsupported', + utf8 + ? `CAB filename is not valid UTF-8${error instanceof Error ? `: ${error.message}` : '.'}` + : 'A non-ASCII CAB filename is not valid UTF-8; no legacy code-page guess was made.', + { offset, structure: 'CFFILE.szName' } + ); + } +} + +export interface NormalizedCabPath { + path: string; + normalizedPath: string; + duplicateKey: string; +} + +export function normalizeCabPath( + path: string, + limits: OnePkgLimits, + offset: number +): NormalizedCabPath { + if (path.length === 0) { + fail('cab-path-empty', 'CAB entry has an empty path.', { + offset, + structure: 'CFFILE.szName', + }); + } + if (path.length > limits.maxPathCharacters) { + fail( + 'cab-path-too-long', + `CAB path exceeds the configured ${limits.maxPathCharacters}-character limit.`, + { offset, structure: 'CFFILE.szName' } + ); + } + if (/^[\\/]/u.test(path) || /^[A-Za-z]:/u.test(path)) { + fail('cab-path-absolute', `Absolute CAB path is not allowed: ${path}`, { + offset, + structure: 'CFFILE.szName', + }); + } + + const slashPath = path.replaceAll('\\', '/'); + const rawSegments = slashPath.split('/'); + if (rawSegments.length > limits.maxPathDepth) { + fail( + 'cab-path-too-deep', + `CAB path exceeds the configured ${limits.maxPathDepth}-segment limit.`, + { offset, structure: 'CFFILE.szName' } + ); + } + + const segments = rawSegments.map((segment) => { + if (segment.length === 0 || segment === '.' || segment === '..') { + fail( + 'cab-path-invalid-segment', + `CAB path contains an unsafe or empty segment: ${path}`, + { offset, structure: 'CFFILE.szName' } + ); + } + if ( + [...segment].some((character) => { + const codePoint = character.codePointAt(0)!; + return codePoint <= 0x1f || codePoint === 0x7f; + }) + ) { + fail( + 'cab-path-control-character', + `CAB path contains a control character: ${path}`, + { offset, structure: 'CFFILE.szName' } + ); + } + return segment.normalize('NFC'); + }); + + const normalizedPath = segments.join('/'); + return { + path, + normalizedPath, + // CAB paths have Windows semantics. NFC + lower-case catches the common + // duplicate aliases without writing any archive path to the filesystem. + duplicateKey: normalizedPath.toLowerCase(), + }; +} diff --git a/src/onenote/onepkg/types.ts b/src/onenote/onepkg/types.ts new file mode 100644 index 0000000..94119fa --- /dev/null +++ b/src/onenote/onepkg/types.ts @@ -0,0 +1,120 @@ +import type { ParserDiagnostic } from '../model/diagnostics.js'; + +export type CabCompressionMethod = + | { kind: 'none'; raw: number; label: 'None' } + | { kind: 'mszip'; raw: number; label: 'MSZIP' } + | { + kind: 'quantum'; + raw: number; + label: 'Quantum'; + level: number; + memoryBits: number; + } + | { + kind: 'lzx'; + raw: number; + label: 'LZX'; + windowBits: number; + windowBytes: number; + }; + +export interface CabFileEntry { + index: number; + path: string; + normalizedPath: string; + folderIndex: number; + uncompressedOffset: number; + uncompressedSize: number; + attributes: number; + compression: CabCompressionMethod; +} + +export interface CabFolderSummary { + index: number; + dataBlockCount: number; + compressedSize: number; + uncompressedSize: number; + compression: CabCompressionMethod; +} + +export interface CabEnumerationResult { + cabinetSetId: number; + cabinetSetIndex: number; + declaredSize: number; + entries: CabFileEntry[]; + folders: CabFolderSummary[]; + diagnostics: ParserDiagnostic[]; +} + +export interface ExtractedCabFile extends CabFileEntry { + bytes: Uint8Array; +} + +export interface CabExtractionResult extends CabEnumerationResult { + extractedEntries: ExtractedCabFile[]; +} + +export interface OnePkgCancellation { + signal?: AbortSignal; + isCancelled?: () => boolean; + /** Called between CAB data blocks so a worker can yield to its event loop. */ + yield?: () => void | Promise; +} + +export interface OnePkgLimits { + maxCabinetBytes: number; + maxFolders: number; + maxFiles: number; + maxDataBlocks: number; + maxNameBytes: number; + maxPathCharacters: number; + maxPathDepth: number; + maxFileBytes: number; + maxFolderUncompressedBytes: number; + maxTotalUncompressedBytes: number; + maxExtractedBytes: number; + /** Maximum per-folder uncompressed CFDATA bytes / compressed payload bytes. */ + maxCompressionRatio: number; + maxLzxWindowBytes: number; + maxDecodeOperations: number; +} + +export const DEFAULT_ONEPKG_LIMITS: Readonly = { + maxCabinetBytes: 512 * 1024 * 1024, + maxFolders: 1_024, + maxFiles: 10_000, + maxDataBlocks: 131_072, + maxNameBytes: 255, + maxPathCharacters: 4_096, + maxPathDepth: 64, + maxFileBytes: 256 * 1024 * 1024, + maxFolderUncompressedBytes: 512 * 1024 * 1024, + maxTotalUncompressedBytes: 512 * 1024 * 1024, + maxExtractedBytes: 512 * 1024 * 1024, + maxCompressionRatio: 200, + maxLzxWindowBytes: 8 * 1024 * 1024, + maxDecodeOperations: 1_000_000_000, +}; + +export interface CabOpenOptions { + limits?: Partial; + cancellation?: OnePkgCancellation; +} + +export function resolveLimits( + limits: Partial | undefined +): OnePkgLimits { + const resolved = { ...DEFAULT_ONEPKG_LIMITS, ...limits }; + for (const [name, value] of Object.entries(resolved)) { + if (name === 'maxCompressionRatio') { + if (!Number.isFinite(value) || value <= 0) { + throw new TypeError(`${name} must be a positive finite number`); + } + continue; + } + if (!Number.isSafeInteger(value) || value <= 0) { + throw new TypeError(`${name} must be a positive safe integer`); + } + } + return resolved; +} diff --git a/src/onenote/onestore/desktop-store.ts b/src/onenote/onestore/desktop-store.ts new file mode 100644 index 0000000..2e1d0dd --- /dev/null +++ b/src/onenote/onestore/desktop-store.ts @@ -0,0 +1,560 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 +// +// Portions adapted from onenote.rs (MPL-2.0), revision +// 5138a39a3f4e72b840932f9872fecde52fa9da60: `onestore/desktop/ +// one_store_file.rs` and `onestore/desktop/objects/{global_id_table,object, +// object_group_list,object_space,revision,revision_manifest_list, +// root_file_node_list}.rs`. Deviations: selected section nodes only, +// serializable diagnostics, explicit limits, and attachment payloads skipped. + +import { BinaryReader } from '../binary/BinaryReader.js'; +import { readGuid } from '../binary/guid.js'; +import type { ParserDiagnostic } from '../model/diagnostics.js'; +import { + DESKTOP_REVISION_STORE_FORMAT, + ONE_SECTION_FILE_TYPE, + detectOneNoteFormat, +} from '../parser/detect-format.js'; +import { OneNoteParserError } from '../parser/error.js'; +import type { OneNoteParserLimits } from '../parser/limits.js'; +import { + DesktopFileNodeParser, + readChunkReference64x32, + readTransactionNodeCounts, +} from './file-node.js'; +import { parseObjectPropSet } from './property-set.js'; +import type { + CompactId, + ExGuid, + FileNode, + FileNodeList, + ObjectSpace, + StoreObject, +} from './types.js'; +import { exGuidKey } from './types.js'; + +export interface DesktopOneStore { + rootObjectSpace: ObjectSpace; + objectSpaces: Map; +} + +interface ParseContext { + bytes: Uint8Array; + limits: OneNoteParserLimits; + diagnostics: ParserDiagnostic[]; + objectCount: number; +} + +export function parseDesktopOneStore( + bytes: Uint8Array, + limits: OneNoteParserLimits, + diagnostics: ParserDiagnostic[] +): DesktopOneStore { + if (bytes.byteLength > limits.maxFileBytes) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `OneNote section is ${bytes.byteLength} bytes (limit ${limits.maxFileBytes})`, + { structure: 'OneStore file' } + ); + } + + const detected = detectOneNoteFormat(bytes); + if (detected.kind !== 'desktop-one') { + throw new OneNoteParserError( + detected.kind === 'not-onenote' ? 'INVALID_FORMAT' : 'UNSUPPORTED_FORMAT', + detected.kind === 'fsshttp-one' + ? 'This OneNote section uses FSSHTTP packaging, which is not yet supported' + : `Expected a desktop .one section; detected ${detected.kind}`, + { structure: 'OneStore header' } + ); + } + if (bytes.byteLength < 1024) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'OneStore header is shorter than 1024 bytes', + { structure: 'OneStore header' } + ); + } + + const header = new BinaryReader(bytes, 0, 1024, 'OneStore header'); + const fileType = readGuid(header); + header.skip(32); + const fileFormat = readGuid(header); + if ( + fileType !== ONE_SECTION_FILE_TYPE || + fileFormat !== DESKTOP_REVISION_STORE_FORMAT + ) { + throw new OneNoteParserError( + 'INVALID_FORMAT', + 'The OneStore header changed during format validation', + { structure: 'OneStore header' } + ); + } + header.seek(160); + const transactionLogReference = readChunkReference64x32(header); + const rootReference = readChunkReference64x32(header); + const nodeCounts = readTransactionNodeCounts( + bytes, + transactionLogReference, + limits + ); + const fileNodeParser = new DesktopFileNodeParser(bytes, limits, nodeCounts); + const rootList = fileNodeParser.parseList(rootReference); + const context: ParseContext = { + bytes, + limits, + diagnostics, + objectCount: 0, + }; + + let rootObjectSpaceId: ExGuid | undefined; + const objectSpaces = new Map(); + + for (const node of rootList.nodes) { + if (node.id === 0x004) { + rootObjectSpaceId = readExGuid(nodeReader(context, node)); + } else if (node.id === 0x008) { + const objectSpace = parseObjectSpace(context, node); + objectSpaces.set(exGuidKey(objectSpace.id), objectSpace); + } else if (node.id !== 0x090 && node.id !== 0x0ff) { + diagnostic( + context, + 'UNSUPPORTED_ROOT_NODE', + `Skipped unsupported root FileNode 0x${node.id.toString(16)}`, + node + ); + } + } + + if (!rootObjectSpaceId) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Root FileNodeList has no ObjectSpaceManifestRootFND', + { structure: 'RootFileNodeList' } + ); + } + const rootObjectSpace = objectSpaces.get(exGuidKey(rootObjectSpaceId)); + if (!rootObjectSpace) { + throw new OneNoteParserError( + 'MISSING_REFERENCE', + `Root object space ${exGuidKey(rootObjectSpaceId)} is absent`, + { structure: 'RootFileNodeList' } + ); + } + + return { rootObjectSpace, objectSpaces }; +} + +function parseObjectSpace(context: ParseContext, node: FileNode): ObjectSpace { + const id = readExGuid(nodeReader(context, node)); + const manifest = requiredChildList( + node, + 'ObjectSpaceManifestListReferenceFND' + ); + const revisions = manifest.nodes.filter((item) => item.id === 0x010); + const lastRevision = revisions.at(-1); + if (!lastRevision) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Object-space manifest has no revision-manifest reference', + { offset: node.offset, structure: 'ObjectSpaceManifestList' } + ); + } + + const roots = new Map(); + const objects = new Map(); + parseRevisionManifestList( + context, + requiredChildList(lastRevision, 'RevisionManifestListReferenceFND'), + id, + roots, + objects + ); + return { id, roots, objects }; +} + +function parseRevisionManifestList( + context: ParseContext, + list: FileNodeList, + contextId: ExGuid, + roots: Map, + objects: Map +): void { + if (list.nodes[0]?.id !== 0x014) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'RevisionManifestList does not begin with RevisionManifestListStartFND', + { structure: 'RevisionManifestList' } + ); + } + + let index = 1; + while (index < list.nodes.length) { + const node = list.nodes[index]!; + if (node.id === 0x01c) { + index += 1; + } else if (node.id === 0x05c || node.id === 0x05d) { + index += 1; + } else if (node.id === 0x01b || node.id === 0x01e || node.id === 0x01f) { + index = parseRevision( + context, + list.nodes, + index + 1, + contextId, + roots, + objects + ); + } else { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Unexpected FileNode 0x${node.id.toString(16)} in RevisionManifestList`, + { offset: node.offset, structure: 'RevisionManifestList' } + ); + } + } +} + +function parseRevision( + context: ParseContext, + nodes: FileNode[], + startIndex: number, + contextId: ExGuid, + roots: Map, + objects: Map +): number { + let index = startIndex; + let mostRecentMapping: ReadonlyMap | undefined; + + while (index < nodes.length) { + const node = nodes[index]!; + if (node.id === 0x01c) return index + 1; + + if (node.id === 0x0b0) { + parseObjectGroup( + context, + requiredChildList(node, 'ObjectGroupListReferenceFND'), + contextId, + objects + ); + index += 1; + if (nodes[index]?.id === 0x084) index += 1; + continue; + } + if (node.id === 0x0b4) { + index = parseInlineObjectGroup(context, nodes, index, contextId, objects); + continue; + } + if (node.id === 0x021 || node.id === 0x022) { + const result = parseGlobalIdTable(context, nodes, index); + index = result.nextIndex; + mostRecentMapping = result.mapping; + while (index < nodes.length && isObjectDeclaration(nodes[index]!.id)) { + parseObjectDeclaration( + context, + nodes[index]!, + contextId, + mostRecentMapping, + objects + ); + index += 1; + if (nodes[index]?.id === 0x084) index += 1; + } + continue; + } + if (node.id === 0x05a) { + const reader = nodeReader(context, node); + const objectId = readExGuid(reader); + const role = reader.u32(); + if (role !== 1 && role !== 2 && role !== 4) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Invalid object-space root role ${role}`, + { offset: node.offset, structure: 'RootObjectReference3FND' } + ); + } + if (!roots.has(role)) roots.set(role, objectId); + index += 1; + continue; + } + if (node.id === 0x059) { + if (!mostRecentMapping) { + throw new OneNoteParserError( + 'MISSING_REFERENCE', + 'Compact root reference appears before a GlobalIdTable', + { offset: node.offset, structure: 'RootObjectReference2FNDX' } + ); + } + const reader = nodeReader(context, node); + const objectId = resolveCompactId( + readCompactId(reader), + mostRecentMapping + ); + const role = reader.u32(); + if (!roots.has(role)) roots.set(role, objectId); + index += 1; + continue; + } + if (node.id === 0x08c || node.id === 0x084) { + index += 1; + continue; + } + + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Unexpected FileNode 0x${node.id.toString(16)} in revision`, + { offset: node.offset, structure: 'Revision' } + ); + } + + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Revision has no RevisionManifestEndFND', + { structure: 'Revision' } + ); +} + +function parseObjectGroup( + context: ParseContext, + list: FileNodeList, + contextId: ExGuid, + objects: Map +): void { + const consumed = parseInlineObjectGroup( + context, + list.nodes, + 0, + contextId, + objects + ); + if (consumed !== list.nodes.length) { + const node = list.nodes[consumed]; + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'ObjectGroupList contains data after ObjectGroupEndFND', + { offset: node?.offset, structure: 'ObjectGroupList' } + ); + } +} + +function parseInlineObjectGroup( + context: ParseContext, + nodes: FileNode[], + startIndex: number, + contextId: ExGuid, + objects: Map +): number { + if (nodes[startIndex]?.id !== 0x0b4) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'ObjectGroupList does not begin with ObjectGroupStartFND', + { structure: 'ObjectGroupList' } + ); + } + const table = parseGlobalIdTable(context, nodes, startIndex + 1); + let index = table.nextIndex; + + while (index < nodes.length && nodes[index]!.id !== 0x0b8) { + const node = nodes[index]!; + if (isObjectDeclaration(node.id)) { + parseObjectDeclaration(context, node, contextId, table.mapping, objects); + } else if (node.id !== 0x08c) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Unexpected FileNode 0x${node.id.toString(16)} in ObjectGroupList`, + { offset: node.offset, structure: 'ObjectGroupList' } + ); + } + index += 1; + } + if (nodes[index]?.id !== 0x0b8) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'ObjectGroupList has no ObjectGroupEndFND', + { structure: 'ObjectGroupList' } + ); + } + return index + 1; +} + +function parseGlobalIdTable( + context: ParseContext, + nodes: FileNode[], + startIndex: number +): { mapping: ReadonlyMap; nextIndex: number } { + const start = nodes[startIndex]; + if (!start || (start.id !== 0x021 && start.id !== 0x022)) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Expected a GlobalIdTable start node', + { structure: 'GlobalIdTable' } + ); + } + + const mapping = new Map(); + let index = startIndex + 1; + while (index < nodes.length) { + const node = nodes[index]!; + if (node.id === 0x028) { + return { mapping, nextIndex: index + 1 }; + } + if (node.id === 0x024) { + const reader = nodeReader(context, node); + mapping.set(reader.u32(), readGuid(reader)); + } else if (node.id === 0x025 || node.id === 0x026) { + diagnostic( + context, + 'UNSUPPORTED_GLOBAL_ID_MAPPING', + `Skipped dependent GlobalIdTable entry 0x${node.id.toString(16)}`, + node + ); + } else { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Unexpected node 0x${node.id.toString(16)} in GlobalIdTable`, + { offset: node.offset, structure: 'GlobalIdTable' } + ); + } + index += 1; + } + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'GlobalIdTable has no end node', + { structure: 'GlobalIdTable' } + ); +} + +function parseObjectDeclaration( + context: ParseContext, + node: FileNode, + contextId: ExGuid, + mapping: ReadonlyMap, + objects: Map +): void { + if (node.id === 0x072 || node.id === 0x073) { + diagnostic( + context, + 'UNSUPPORTED_ATTACHMENT_DECLARATION', + 'Attachment object metadata is not extracted in this parser phase', + node + ); + return; + } + if (!node.dataReference) { + throw new OneNoteParserError( + 'MISSING_REFERENCE', + 'Object declaration has no property-set reference', + { offset: node.offset, structure: 'ObjectDeclaration2RefCountFND' } + ); + } + + const reader = nodeReader(context, node); + const compactId = readCompactId(reader); + const jcid = reader.u32(); + reader.u8(); // reference-stream presence flags + if (node.id === 0x0a4) reader.u8(); + else if (node.id === 0x0a5) reader.u32(); + else if (node.id === 0x0c4) { + reader.u8(); + reader.skip(16); + } else if (node.id === 0x0c5) { + reader.u32(); + reader.skip(16); + } + + const id = resolveCompactId(compactId, mapping); + const props = parseObjectPropSet( + context.bytes, + node.dataReference, + context.limits + ); + objects.set(exGuidKey(id), { + id, + jcid, + props, + mapping: new Map(mapping), + contextId, + }); + context.objectCount += 1; + if (context.objectCount > context.limits.maxObjects) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Object count exceeds ${context.limits.maxObjects}`, + { offset: node.offset, structure: 'ObjectDeclaration' } + ); + } +} + +function isObjectDeclaration(nodeId: number): boolean { + return ( + nodeId === 0x0a4 || + nodeId === 0x0a5 || + nodeId === 0x0c4 || + nodeId === 0x0c5 || + nodeId === 0x072 || + nodeId === 0x073 + ); +} + +function resolveCompactId( + compact: CompactId, + mapping: ReadonlyMap +): ExGuid { + const guid = mapping.get(compact.guidIndex); + if (!guid) { + throw new OneNoteParserError( + 'MISSING_REFERENCE', + `GlobalIdTable has no GUID at index ${compact.guidIndex}`, + { structure: 'CompactId' } + ); + } + return { guid, value: compact.value }; +} + +function readCompactId(reader: BinaryReader): CompactId { + const raw = reader.u32(); + return { value: raw & 0xff, guidIndex: raw >>> 8 }; +} + +function readExGuid(reader: BinaryReader): ExGuid { + return { guid: readGuid(reader), value: reader.u32() }; +} + +function requiredChildList(node: FileNode, structure: string): FileNodeList { + if (!node.childList) { + throw new OneNoteParserError( + 'MISSING_REFERENCE', + `${structure} does not point to a FileNodeList`, + { offset: node.offset, structure } + ); + } + return node.childList; +} + +function nodeReader(context: ParseContext, node: FileNode): BinaryReader { + return new BinaryReader( + context.bytes, + node.payloadOffset, + node.payloadLength, + `FileNode 0x${node.id.toString(16)}` + ); +} + +function diagnostic( + context: ParseContext, + code: string, + message: string, + node: FileNode +): void { + if (context.diagnostics.length >= context.limits.maxDiagnostics) return; + context.diagnostics.push({ + severity: 'warning', + code, + message, + offset: node.offset, + structure: `FileNode 0x${node.id.toString(16)}`, + recoverable: true, + }); +} diff --git a/src/onenote/onestore/file-node.ts b/src/onenote/onestore/file-node.ts new file mode 100644 index 0000000..cd828c0 --- /dev/null +++ b/src/onenote/onestore/file-node.ts @@ -0,0 +1,440 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 +// +// Portions adapted from onenote.rs (MPL-2.0), revision +// 5138a39a3f4e72b840932f9872fecde52fa9da60: `onestore/desktop/file_structure/ +// {file_node_list_fragment,parse_context,transaction_log_fragment}.rs`, +// `onestore/desktop/file_node/{mod,file_node_chunk_reference}.rs`, and +// `onestore/desktop/common/file_chunk_reference.rs`. Deviations: strict byte +// bounds, cycle/depth/count limits, and no eager decoding of unrelated data. + +import { BinaryReader } from '../binary/BinaryReader.js'; +import { OneNoteParserError } from '../parser/error.js'; +import type { OneNoteParserLimits } from '../parser/limits.js'; +import type { ChunkReference, FileNode, FileNodeList } from './types.js'; + +const FILE_NODE_LIST_MAGIC = 0xa4567ab1f5f7f4c4n; +const FILE_NODE_LIST_FOOTER = 0x8bc215c38233ba4bn; +const NIL_U64 = 0xffffffffffffffffn; + +export interface OneStoreHeader { + fileType: string; + legacyFileVersion: string; + fileFormat: string; + rootList: ChunkReference; +} + +interface ListParseState { + fragments: number; + nodes: number; +} + +export class DesktopFileNodeParser { + private readonly root: BinaryReader; + private readonly limits: OneNoteParserLimits; + private readonly listCache = new Map(); + private readonly activeFragments = new Set(); + private readonly state: ListParseState = { fragments: 0, nodes: 0 }; + private readonly remainingNodeCounts: Map; + + constructor( + bytes: Uint8Array, + limits: OneNoteParserLimits, + nodeCounts: ReadonlyMap = new Map() + ) { + this.root = new BinaryReader(bytes, 0, bytes.byteLength, 'OneStore file'); + this.limits = limits; + this.remainingNodeCounts = new Map(nodeCounts); + } + + parseList(reference: ChunkReference, depth = 0): FileNodeList { + if (reference.nil || reference.zero || reference.size === 0) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'A required FileNodeList reference is nil or empty', + { offset: reference.offset, structure: 'FileNodeList' } + ); + } + if (depth > this.limits.maxListDepth) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `FileNodeList nesting exceeds ${this.limits.maxListDepth}`, + { offset: reference.offset, structure: 'FileNodeList' } + ); + } + + const initialKey = referenceKey(reference); + const cached = this.listCache.get(initialKey); + if (cached) return cached; + + const nodes: FileNode[] = []; + const seenFragments = new Set(); + let current = reference; + let listId: number | undefined; + let expectedSequence = 0; + + while (!current.nil && !current.zero && current.size !== 0) { + this.state.fragments += 1; + if (this.state.fragments > this.limits.maxListFragments) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `FileNodeList fragment count exceeds ${this.limits.maxListFragments}`, + { offset: current.offset, structure: 'FileNodeList' } + ); + } + + const key = referenceKey(current); + if (seenFragments.has(key)) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Cycle detected in a FileNodeList fragment chain', + { offset: current.offset, structure: 'FileNodeList' } + ); + } + seenFragments.add(key); + if (this.activeFragments.has(key)) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Cycle detected in FileNodeList fragments', + { offset: current.offset, structure: 'FileNodeList' } + ); + } + this.activeFragments.add(key); + + try { + const fragment = this.root.viewAt( + current.offset, + current.size, + 'FileNodeListFragment' + ); + if (fragment.remaining < 36) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'FileNodeListFragment is shorter than its 36-byte framing', + { offset: current.offset, structure: 'FileNodeListFragment' } + ); + } + if (fragment.u64() !== FILE_NODE_LIST_MAGIC) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Invalid FileNodeListFragment magic', + { offset: current.offset, structure: 'FileNodeListFragment' } + ); + } + + const fragmentListId = fragment.u32(); + const sequence = fragment.u32(); + if (listId === undefined) listId = fragmentListId; + if (fragmentListId !== listId || sequence !== expectedSequence) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Inconsistent FileNodeList fragment identity or sequence', + { offset: current.offset, structure: 'FileNodeListFragment' } + ); + } + expectedSequence += 1; + let remainingNodeCount = this.remainingNodeCounts.get(fragmentListId); + + const nodeRegionEnd = fragment.end - 20; + while ( + fragment.offset + 4 <= nodeRegionEnd && + (remainingNodeCount === undefined || remainingNodeCount > 0) + ) { + const firstWord = new DataView( + fragment.bytes.buffer, + fragment.bytes.byteOffset + fragment.offset, + 4 + ).getUint32(0, true); + if (firstWord === 0) { + fragment.skip(4); + continue; + } + + const node = this.parseNode(fragment, nodeRegionEnd, depth); + this.state.nodes += 1; + if (this.state.nodes > this.limits.maxFileNodes) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `FileNode count exceeds ${this.limits.maxFileNodes}`, + { offset: node.offset, structure: 'FileNode' } + ); + } + if (node.id === 0x0ff) { + // ChunkTerminatorFND ends logical decoding. Remaining bytes in the + // node region are opaque padding and need not contain zero words. + fragment.skip(nodeRegionEnd - fragment.offset); + break; + } + if (node.id !== 0) { + nodes.push(node); + if (remainingNodeCount !== undefined) remainingNodeCount -= 1; + } + } + if (remainingNodeCount !== undefined) + this.remainingNodeCounts.set(fragmentListId, remainingNodeCount); + + // The fragment format permits up to three trailing padding bytes. + if (fragment.offset < nodeRegionEnd) + fragment.skip(nodeRegionEnd - fragment.offset); + + current = readChunkReference64x32(fragment); + if (fragment.u64() !== FILE_NODE_LIST_FOOTER) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Invalid FileNodeListFragment footer', + { offset: fragment.offset - 8, structure: 'FileNodeListFragment' } + ); + } + } finally { + this.activeFragments.delete(key); + } + } + + const result: FileNodeList = { id: listId ?? 0, nodes }; + this.listCache.set(initialKey, result); + return result; + } + + private parseNode( + reader: BinaryReader, + nodeRegionEnd: number, + depth: number + ): FileNode { + const offset = reader.offset; + const firstWord = reader.u32(); + const id = firstWord & 0x3ff; + const declaredSize = (firstWord >>> 10) & 0x1fff; + const stpFormat = (firstWord >>> 23) & 0x3; + const cbFormat = (firstWord >>> 25) & 0x3; + const baseType = (firstWord >>> 27) & 0xf; + + if (id === 0) { + return { + id, + declaredSize, + offset, + payloadOffset: reader.offset, + payloadLength: 0, + }; + } + if (declaredSize < 4 || offset + declaredSize > nodeRegionEnd) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `FileNode 0x${id.toString(16)} has invalid declared size ${declaredSize}`, + { offset, structure: 'FileNode' } + ); + } + + let dataReference: ChunkReference | undefined; + let childList: FileNodeList | undefined; + if (baseType === 1 || baseType === 2) { + dataReference = readNodeChunkReference(reader, stpFormat, cbFormat); + if (baseType === 2) { + childList = this.parseList(dataReference, depth + 1); + } + } else if (baseType !== 0) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Unsupported FileNode base type ${baseType}`, + { offset, structure: 'FileNode' } + ); + } + + const consumed = reader.offset - offset; + const payloadLength = declaredSize - consumed; + if (payloadLength < 0) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'FileNode reference exceeds the declared node size', + { offset, structure: 'FileNode' } + ); + } + const payloadOffset = reader.offset; + reader.skip(payloadLength); + + return { + id, + declaredSize, + offset, + payloadOffset, + payloadLength, + dataReference, + childList, + }; + } +} + +export function readChunkReference64x32(reader: BinaryReader): ChunkReference { + const rawOffset = reader.u64(); + const rawSize = reader.u32(); + return chunkReference(rawOffset, BigInt(rawSize), rawOffset === NIL_U64); +} + +/** Read transaction-log node counts used to delimit opaque fragment padding. */ +export function readTransactionNodeCounts( + bytes: Uint8Array, + reference: ChunkReference, + limits: OneNoteParserLimits +): Map { + const root = new BinaryReader(bytes, 0, bytes.byteLength, 'OneStore file'); + const result = new Map(); + const seen = new Set(); + let current = reference; + let fragments = 0; + let entries = 0; + + while (!current.nil && !current.zero && current.size !== 0) { + fragments += 1; + if (fragments > limits.maxTransactionFragments) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Transaction-log fragment count exceeds ${limits.maxTransactionFragments}`, + { offset: current.offset, structure: 'TransactionLog' } + ); + } + const key = referenceKey(current); + if (seen.has(key)) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Cycle detected in transaction-log fragments', + { offset: current.offset, structure: 'TransactionLog' } + ); + } + seen.add(key); + + const fragment = root.viewAt( + current.offset, + current.size, + 'TransactionLogFragment' + ); + if (fragment.remaining < 12) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'TransactionLogFragment has an invalid size', + { offset: current.offset, structure: 'TransactionLogFragment' } + ); + } + const count = Math.floor((fragment.remaining - 12) / 8); + entries += count; + if (entries > limits.maxTransactionEntries) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Transaction-log entry count exceeds ${limits.maxTransactionEntries}`, + { offset: current.offset, structure: 'TransactionLog' } + ); + } + + for (let index = 0; index < count; index += 1) { + const listId = fragment.u32(); + const nodeCount = fragment.u32(); + if (listId === 1) continue; // TransactionEntry sentinel. + result.set(listId, Math.max(result.get(listId) ?? 0, nodeCount)); + } + current = readChunkReference64x32(fragment); + // Transaction fragments are allocation-aligned and can carry up to seven + // unused bytes after the next-fragment reference. + if (fragment.remaining > 0) fragment.skip(fragment.remaining); + } + + return result; +} + +function readNodeChunkReference( + reader: BinaryReader, + stpFormat: number, + cbFormat: number +): ChunkReference { + let rawOffset: bigint; + let offsetNil: boolean; + switch (stpFormat) { + case 0: + rawOffset = reader.u64(); + offsetNil = rawOffset === NIL_U64; + break; + case 1: { + const value = reader.u32(); + rawOffset = BigInt(value); + offsetNil = value === 0xffffffff; + break; + } + case 2: { + const value = reader.u16(); + rawOffset = BigInt(value) * 8n; + offsetNil = value === 0xffff; + break; + } + case 3: { + const value = reader.u32(); + rawOffset = BigInt(value) * 8n; + offsetNil = value === 0xffffffff; + break; + } + default: + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Invalid FileNode stp format ${stpFormat}`, + { offset: reader.offset, structure: 'FileNodeChunkReference' } + ); + } + + let rawSize: bigint; + switch (cbFormat) { + case 0: + rawSize = BigInt(reader.u32()); + break; + case 1: + rawSize = reader.u64(); + break; + case 2: + rawSize = BigInt(reader.u8()) * 8n; + break; + case 3: + rawSize = BigInt(reader.u16()) * 8n; + break; + default: + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Invalid FileNode cb format ${cbFormat}`, + { offset: reader.offset, structure: 'FileNodeChunkReference' } + ); + } + + return chunkReference(rawOffset, rawSize, offsetNil); +} + +function chunkReference( + rawOffset: bigint, + rawSize: bigint, + nil: boolean +): ChunkReference { + if (nil && rawSize === 0n) { + return { offset: 0, size: 0, nil: true, zero: false }; + } + if (rawOffset > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new OneNoteParserError( + 'BOUNDS_EXCEEDED', + `Chunk offset is too large: ${rawOffset}`, + { structure: 'FileChunkReference' } + ); + } + if (rawSize > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new OneNoteParserError( + 'BOUNDS_EXCEEDED', + `Chunk size is too large: ${rawSize}`, + { structure: 'FileChunkReference' } + ); + } + + return { + offset: Number(rawOffset), + size: Number(rawSize), + nil: nil && rawSize === 0n, + zero: rawOffset === 0n && rawSize === 0n, + }; +} + +function referenceKey(reference: ChunkReference): string { + return `${reference.offset}:${reference.size}`; +} diff --git a/src/onenote/onestore/property-set.ts b/src/onenote/onestore/property-set.ts new file mode 100644 index 0000000..86ffefc --- /dev/null +++ b/src/onenote/onestore/property-set.ts @@ -0,0 +1,290 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 +// +// Portions adapted from onenote.rs (MPL-2.0), revision +// 5138a39a3f4e72b840932f9872fecde52fa9da60: `onestore/shared/ +// {object_prop_set,object_stream_header,prop_set,property}.rs`. Deviations: +// recursive/count/vector reads are bounded and unresolved references are +// never silently discarded. + +import { BinaryReader } from '../binary/BinaryReader.js'; +import { OneNoteParserError } from '../parser/error.js'; +import type { OneNoteParserLimits } from '../parser/limits.js'; +import type { + ChunkReference, + CompactId, + ObjectPropSet, + PropertyEntry, + PropertySet, + PropertyValue, +} from './types.js'; + +interface ObjectStreamHeader { + count: number; + extendedStreamsPresent: boolean; + osidStreamNotPresent: boolean; +} + +export function parseObjectPropSet( + bytes: Uint8Array, + reference: ChunkReference, + limits: OneNoteParserLimits +): ObjectPropSet { + if (reference.nil || reference.zero) { + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + 'Object declaration has a nil property-set reference', + { offset: reference.offset, structure: 'ObjectPropSet' } + ); + } + const reader = new BinaryReader( + bytes, + reference.offset, + reference.size, + 'ObjectPropSet' + ); + + const oidHeader = readStreamHeader(reader, limits); + const objectIds = readCompactIds(reader, oidHeader.count); + let objectSpaceIds: CompactId[] = []; + let contextIds: CompactId[] = []; + + if (!oidHeader.osidStreamNotPresent) { + const osidHeader = readStreamHeader(reader, limits); + objectSpaceIds = readCompactIds(reader, osidHeader.count); + if (osidHeader.extendedStreamsPresent) { + const contextHeader = readStreamHeader(reader, limits); + contextIds = readCompactIds(reader, contextHeader.count); + } + } + + const properties = readPropertySet(reader, limits, 0); + return { objectIds, objectSpaceIds, contextIds, properties }; +} + +export function findProperty( + props: ObjectPropSet | PropertySet, + rawPropertyId: number +): PropertyEntry | undefined { + const set = 'properties' in props ? props.properties : props; + const id = rawPropertyId & 0x03ffffff; + return set.entries.find((entry) => entry.id === id); +} + +export function countObjectReferences(value: PropertyValue): number { + switch (value.kind) { + case 'objectId': + return 1; + case 'objectIds': + return value.count; + case 'propertyValues': + return value.sets.reduce( + (total, set) => + total + countReferencesInSet(set, countObjectReferences), + 0 + ); + case 'propertySet': + return countReferencesInSet(value.set, countObjectReferences); + default: + return 0; + } +} + +export function countObjectSpaceReferences(value: PropertyValue): number { + switch (value.kind) { + case 'objectSpaceId': + return 1; + case 'objectSpaceIds': + return value.count; + case 'propertyValues': + return value.sets.reduce( + (total, set) => + total + countReferencesInSet(set, countObjectSpaceReferences), + 0 + ); + case 'propertySet': + return countReferencesInSet(value.set, countObjectSpaceReferences); + default: + return 0; + } +} + +export function referenceOffset( + props: ObjectPropSet, + rawPropertyId: number, + counter: (value: PropertyValue) => number +): number { + const targetId = rawPropertyId & 0x03ffffff; + const index = props.properties.entries.findIndex( + (entry) => entry.id === targetId + ); + if (index < 0) { + throw new OneNoteParserError( + 'MISSING_REFERENCE', + `Property 0x${rawPropertyId.toString(16)} is absent`, + { structure: 'ObjectPropSet' } + ); + } + return props.properties.entries + .slice(0, index) + .reduce((total, entry) => total + counter(entry.value), 0); +} + +function readStreamHeader( + reader: BinaryReader, + limits: OneNoteParserLimits +): ObjectStreamHeader { + const raw = reader.u32(); + const count = raw & 0x00ffffff; + if (count > limits.maxStreamIds) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Object stream contains ${count} IDs (limit ${limits.maxStreamIds})`, + { offset: reader.offset - 4, structure: 'ObjectStreamHeader' } + ); + } + return { + count, + extendedStreamsPresent: ((raw >>> 30) & 1) !== 0, + osidStreamNotPresent: raw >>> 31 !== 0, + }; +} + +function readCompactIds(reader: BinaryReader, count: number): CompactId[] { + const ids: CompactId[] = []; + for (let index = 0; index < count; index += 1) { + const raw = reader.u32(); + ids.push({ value: raw & 0xff, guidIndex: raw >>> 8 }); + } + return ids; +} + +function readPropertySet( + reader: BinaryReader, + limits: OneNoteParserLimits, + depth: number +): PropertySet { + if (depth > limits.maxPropertyDepth) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Nested property depth exceeds ${limits.maxPropertyDepth}`, + { offset: reader.offset, structure: 'PropertySet' } + ); + } + + const count = reader.u16(); + if (count > limits.maxPropertiesPerSet) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `PropertySet contains ${count} properties`, + { offset: reader.offset - 2, structure: 'PropertySet' } + ); + } + + const rawIds: number[] = []; + for (let index = 0; index < count; index += 1) rawIds.push(reader.u32()); + + const entries = rawIds.map((rawId) => { + const type = (rawId >>> 26) & 0x1f; + return { + rawId, + id: rawId & 0x03ffffff, + type, + value: readPropertyValue(reader, rawId, type, limits, depth), + } satisfies PropertyEntry; + }); + return { entries }; +} + +function readPropertyValue( + reader: BinaryReader, + rawId: number, + type: number, + limits: OneNoteParserLimits, + depth: number +): PropertyValue { + switch (type) { + case 0x1: + return { kind: 'empty' }; + case 0x2: + return { kind: 'bool', value: rawId >>> 31 === 1 }; + case 0x3: + return { kind: 'u8', value: reader.u8() }; + case 0x4: + return { kind: 'u16', value: reader.u16() }; + case 0x5: + return { kind: 'u32', value: reader.u32() }; + case 0x6: + return { kind: 'u64', value: reader.u64() }; + case 0x7: { + const length = reader.u32(); + if (length > limits.maxVectorBytes) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Property vector is ${length} bytes`, + { offset: reader.offset - 4, structure: 'PropertyValue' } + ); + } + return { kind: 'bytes', value: reader.read(length) }; + } + case 0x8: + return { kind: 'objectId' }; + case 0x9: + return { kind: 'objectIds', count: boundedCount(reader, limits) }; + case 0xa: + return { kind: 'objectSpaceId' }; + case 0xb: + return { + kind: 'objectSpaceIds', + count: boundedCount(reader, limits), + }; + case 0xc: + return { kind: 'contextId' }; + case 0xd: + return { kind: 'contextIds', count: boundedCount(reader, limits) }; + case 0x10: { + const count = boundedCount(reader, limits); + const id = reader.u32(); + const sets: PropertySet[] = []; + for (let index = 0; index < count; index += 1) { + sets.push(readPropertySet(reader, limits, depth + 1)); + } + return { kind: 'propertyValues', id, sets }; + } + case 0x11: + return { + kind: 'propertySet', + set: readPropertySet(reader, limits, depth + 1), + }; + default: + throw new OneNoteParserError( + 'INVALID_STRUCTURE', + `Unexpected property type 0x${type.toString(16)}`, + { offset: reader.offset, structure: 'PropertyValue' } + ); + } +} + +function boundedCount( + reader: BinaryReader, + limits: OneNoteParserLimits +): number { + const count = reader.u32(); + if (count > limits.maxObjectReferences) { + throw new OneNoteParserError( + 'LIMIT_EXCEEDED', + `Reference count ${count} exceeds ${limits.maxObjectReferences}`, + { offset: reader.offset - 4, structure: 'PropertyValue' } + ); + } + return count; +} + +function countReferencesInSet( + set: PropertySet, + counter: (value: PropertyValue) => number +): number { + return set.entries.reduce((total, entry) => total + counter(entry.value), 0); +} diff --git a/src/onenote/onestore/types.ts b/src/onenote/onestore/types.ts new file mode 100644 index 0000000..0c1186f --- /dev/null +++ b/src/onenote/onestore/types.ts @@ -0,0 +1,95 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// SPDX-License-Identifier: MPL-2.0 +// +// Portions adapted from onenote.rs (MPL-2.0), revision +// 5138a39a3f4e72b840932f9872fecde52fa9da60: `onestore/desktop/common/ +// {exguid,file_chunk_reference,object_space_object_stream_header}.rs`, +// `onestore/shared/{compact_id,jcid,object_prop_set}.rs`, and desktop object +// modules. This port uses serializable records and explicit limits. + +export interface ExGuid { + guid: string; + value: number; +} + +export interface CompactId { + guidIndex: number; + value: number; +} + +export interface ChunkReference { + offset: number; + size: number; + nil: boolean; + zero: boolean; +} + +export interface FileNodeList { + id: number; + nodes: FileNode[]; +} + +export interface FileNode { + id: number; + declaredSize: number; + offset: number; + payloadOffset: number; + payloadLength: number; + dataReference?: ChunkReference; + childList?: FileNodeList; +} + +export type PropertyValue = + | { kind: 'empty' } + | { kind: 'bool'; value: boolean } + | { kind: 'u8'; value: number } + | { kind: 'u16'; value: number } + | { kind: 'u32'; value: number } + | { kind: 'u64'; value: bigint } + | { kind: 'bytes'; value: Uint8Array } + | { kind: 'objectId' } + | { kind: 'objectIds'; count: number } + | { kind: 'objectSpaceId' } + | { kind: 'objectSpaceIds'; count: number } + | { kind: 'contextId' } + | { kind: 'contextIds'; count: number } + | { kind: 'propertyValues'; id: number; sets: PropertySet[] } + | { kind: 'propertySet'; set: PropertySet }; + +export interface PropertyEntry { + rawId: number; + id: number; + type: number; + value: PropertyValue; +} + +export interface PropertySet { + entries: PropertyEntry[]; +} + +export interface ObjectPropSet { + objectIds: CompactId[]; + objectSpaceIds: CompactId[]; + contextIds: CompactId[]; + properties: PropertySet; +} + +export interface StoreObject { + id: ExGuid; + jcid: number; + props: ObjectPropSet; + mapping: ReadonlyMap; + contextId: ExGuid; +} + +export interface ObjectSpace { + id: ExGuid; + roots: Map; + objects: Map; +} + +export function exGuidKey(id: ExGuid): string { + return `${id.guid}:${id.value}`; +} diff --git a/src/onenote/parser/detect-format.ts b/src/onenote/parser/detect-format.ts new file mode 100644 index 0000000..49734fb --- /dev/null +++ b/src/onenote/parser/detect-format.ts @@ -0,0 +1,70 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import { BinaryReader } from '../binary/BinaryReader.js'; +import { NIL_GUID, readGuid } from '../binary/guid.js'; + +export const ONE_SECTION_FILE_TYPE = '{7B5C52E4-D88C-4DA7-AEB1-5378D02996D3}'; +export const ONE_TOC_FILE_TYPE = '{43FF2FA1-EFD9-4C76-9EE2-10EA5722765F}'; +export const DESKTOP_REVISION_STORE_FORMAT = + '{109ADD3F-911B-49F5-A5D0-1791EDC8AED8}'; +export const FSSHTTP_PACKAGING_FORMAT = + '{638DE92F-A6D4-4BC1-9A36-B3FC2511A5B7}'; + +export type OneNoteFormatKind = + | 'desktop-one' + | 'desktop-onetoc2' + | 'fsshttp-one' + | 'not-onenote' + | 'unknown-onenote'; + +export interface OneNoteFormatDetection { + kind: OneNoteFormatKind; + fileType?: string; + legacyFileVersion?: string; + fileFormat?: string; +} + +export function detectOneNoteFormat( + input: ArrayBuffer | Uint8Array +): OneNoteFormatDetection { + const bytes = toBytes(input); + if (bytes.byteLength < 64) return { kind: 'not-onenote' }; + + const reader = new BinaryReader(bytes, 0, 64, 'OneStore signature'); + const fileType = readGuid(reader); + reader.skip(16); + const legacyFileVersion = readGuid(reader); + const fileFormat = readGuid(reader); + + if (fileType !== ONE_SECTION_FILE_TYPE && fileType !== ONE_TOC_FILE_TYPE) { + return { kind: 'not-onenote', fileType, legacyFileVersion, fileFormat }; + } + if ( + fileFormat === FSSHTTP_PACKAGING_FORMAT || + legacyFileVersion !== NIL_GUID + ) { + return { kind: 'fsshttp-one', fileType, legacyFileVersion, fileFormat }; + } + if (fileFormat !== DESKTOP_REVISION_STORE_FORMAT) { + return { + kind: 'unknown-onenote', + fileType, + legacyFileVersion, + fileFormat, + }; + } + + return { + kind: + fileType === ONE_SECTION_FILE_TYPE ? 'desktop-one' : 'desktop-onetoc2', + fileType, + legacyFileVersion, + fileFormat, + }; +} + +export function toBytes(input: ArrayBuffer | Uint8Array): Uint8Array { + return input instanceof Uint8Array ? input : new Uint8Array(input); +} diff --git a/src/onenote/parser/error.ts b/src/onenote/parser/error.ts new file mode 100644 index 0000000..d5cd384 --- /dev/null +++ b/src/onenote/parser/error.ts @@ -0,0 +1,51 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +export type OneNoteParserErrorCode = + | 'BOUNDS_EXCEEDED' + | 'INVALID_FORMAT' + | 'INVALID_STRUCTURE' + | 'LIMIT_EXCEEDED' + | 'MISSING_REFERENCE' + | 'UNSUPPORTED_FORMAT'; + +export interface OneNoteParserErrorOptions { + offset?: number; + structure?: string; + cause?: unknown; +} + +/** A controlled parse failure safe to serialize across a worker boundary. */ +export class OneNoteParserError extends Error { + readonly code: OneNoteParserErrorCode; + readonly offset?: number; + readonly structure?: string; + + constructor( + code: OneNoteParserErrorCode, + message: string, + options: OneNoteParserErrorOptions = {} + ) { + super(message, { cause: options.cause }); + this.name = 'OneNoteParserError'; + this.code = code; + this.offset = options.offset; + this.structure = options.structure; + } +} + +export function asParserError( + error: unknown, + structure: string +): OneNoteParserError { + if (error instanceof OneNoteParserError) { + return error; + } + + return new OneNoteParserError( + 'INVALID_STRUCTURE', + error instanceof Error ? error.message : String(error), + { cause: error, structure } + ); +} diff --git a/src/onenote/parser/limits.ts b/src/onenote/parser/limits.ts new file mode 100644 index 0000000..68a3b7b --- /dev/null +++ b/src/onenote/parser/limits.ts @@ -0,0 +1,55 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +export interface OneNoteParserLimits { + maxFileBytes: number; + maxTransactionFragments: number; + maxTransactionEntries: number; + maxListFragments: number; + maxListDepth: number; + maxFileNodes: number; + maxObjects: number; + maxPropertiesPerSet: number; + maxPropertyDepth: number; + maxStreamIds: number; + maxVectorBytes: number; + maxObjectReferences: number; + maxGraphDepth: number; + maxPages: number; + maxDiagnostics: number; + maxExtractedTextChars: number; +} + +export const DEFAULT_ONENOTE_PARSER_LIMITS: Readonly = { + maxFileBytes: 512 * 1024 * 1024, + maxTransactionFragments: 16_384, + maxTransactionEntries: 1_000_000, + maxListFragments: 16_384, + maxListDepth: 64, + maxFileNodes: 1_000_000, + maxObjects: 500_000, + maxPropertiesPerSet: 16_384, + maxPropertyDepth: 64, + maxStreamIds: 1_000_000, + maxVectorBytes: 64 * 1024 * 1024, + maxObjectReferences: 1_000_000, + maxGraphDepth: 256, + maxPages: 100_000, + maxDiagnostics: 10_000, + maxExtractedTextChars: 2_000_000, +}; + +export function resolveParserLimits( + limits: Partial = {} +): OneNoteParserLimits { + const resolved = { ...DEFAULT_ONENOTE_PARSER_LIMITS, ...limits }; + + for (const [name, value] of Object.entries(resolved)) { + if (!Number.isSafeInteger(value) || value <= 0) { + throw new TypeError(`${name} must be a positive safe integer`); + } + } + + return resolved; +} diff --git a/src/onenote/parser/parse-section.ts b/src/onenote/parser/parse-section.ts new file mode 100644 index 0000000..9c680dc --- /dev/null +++ b/src/onenote/parser/parse-section.ts @@ -0,0 +1,32 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import type { OneNoteSectionDto } from '../model/dto.js'; +import { buildSectionDto } from '../one/section.js'; +import { parseDesktopOneStore } from '../onestore/desktop-store.js'; +import { toBytes } from './detect-format.js'; +import type { OneNoteParserLimits } from './limits.js'; +import { resolveParserLimits } from './limits.js'; + +export interface ParseOneNoteSectionOptions { + sourceName?: string; + limits?: Partial; +} + +/** Parse a desktop revision-store `.one` section without network access. */ +export function parseOneNoteSection( + input: ArrayBuffer | Uint8Array, + options: ParseOneNoteSectionOptions = {} +): OneNoteSectionDto { + const bytes = toBytes(input); + const limits = resolveParserLimits(options.limits); + const diagnostics: OneNoteSectionDto['diagnostics'] = []; + const store = parseDesktopOneStore(bytes, limits, diagnostics); + return buildSectionDto( + store, + options.sourceName ?? 'Untitled.one', + limits, + diagnostics + ); +} diff --git a/src/source-file.ts b/src/source-file.ts new file mode 100644 index 0000000..aaaabfe --- /dev/null +++ b/src/source-file.ts @@ -0,0 +1,10 @@ +import type { SourceKind } from './worker/onenote.client.js'; + +export const MAX_SOURCE_FILE_BYTES = 512 * 1024 * 1024; + +export function sourceKindFromFileName(fileName: string): SourceKind | null { + const normalized = fileName.toLocaleLowerCase('en-US'); + if (normalized.endsWith('.onepkg')) return 'onepkg'; + if (normalized.endsWith('.one')) return 'one'; + return null; +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..ad22066 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,627 @@ +:root { + color: #241b2b; + background: #f6f3f8; + font-family: + Inter, + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + font-synthesis: none; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +button, +input { + font: inherit; +} + +button { + border: 1px solid #704294; + border-radius: 0.55rem; + padding: 0.55rem 0.9rem; + color: #4c1f6b; + background: #fff; + cursor: pointer; +} + +button:hover:not(:disabled) { + background: #f1e8f8; +} + +button:focus-visible, +summary:focus-visible { + outline: 3px solid #b788d4; + outline-offset: 2px; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +code { + border-radius: 0.25rem; + padding: 0.08rem 0.25rem; + overflow-wrap: anywhere; + background: #eee7f3; +} + +h1, +h2, +p { + overflow-wrap: anywhere; +} + +.application { + display: grid; + gap: 1.25rem; + width: min(76rem, calc(100% - 2rem)); + margin: 2rem auto; +} + +.drop-zone, +.status-card, +.source-toolbar, +.package-inspector, +.diagnostics { + border: 1px solid #c9b9d5; + border-radius: 1rem; + background: #fff; + box-shadow: 0 0.75rem 2.5rem rgb(62 31 85 / 8%); +} + +.drop-zone, +.status-card { + padding: clamp(1.5rem, 4vw, 3rem); +} + +.drop-zone { + border-style: dashed; + text-align: center; +} + +.drop-zone--active { + border-color: #6b368f; + background: #f7effd; +} + +.drop-zone h2, +.status-card h2 { + margin: 0.25rem 0 0.75rem; +} + +.drop-zone__eyebrow, +.status-card__label, +.eyebrow { + margin: 0; + color: #6b368f; + font-size: 0.76rem; + font-weight: 750; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.primary-button { + margin-top: 0.75rem; + color: #fff; + background: #5b2c83; +} + +.primary-button:hover:not(:disabled) { + background: #472164; +} + +.drop-zone__hint { + display: block; + margin-top: 0.75rem; + color: #6f6476; + font-size: 0.9rem; +} + +.status-card--error { + border-color: #b65252; +} + +.status-card--error > div { + margin-top: 1rem; +} + +.source-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1.15rem 1.35rem; +} + +.source-toolbar h2 { + margin: 0.15rem 0 0; + font-size: clamp(1.2rem, 3vw, 1.65rem); +} + +.reader-workspace { + display: grid; + grid-template-columns: minmax(16rem, 0.78fr) minmax(0, 2fr); + min-height: 30rem; + overflow: hidden; + border: 1px solid #c9b9d5; + border-radius: 1rem; + background: #fff; + box-shadow: 0 0.75rem 2.5rem rgb(62 31 85 / 8%); +} + +.source-navigation { + overflow: auto; + border-right: 1px solid #d9cedf; + background: #faf7fc; +} + +.navigation-heading { + position: sticky; + z-index: 1; + top: 0; + padding: 1.2rem; + border-bottom: 1px solid #ded5e3; + background: #faf7fc; +} + +.navigation-heading h2 { + margin: 0.15rem 0 0; + font-size: 1.05rem; +} + +.navigation-heading > p:last-child { + margin: 0.35rem 0 0; + color: #716675; + font-size: 0.8rem; +} + +.page-list, +.section-list, +.diagnostic-list { + margin: 0; + padding: 0; + list-style: none; +} + +.page-list { + padding: 0.55rem; +} + +.page-list button, +.section-list button { + display: grid; + gap: 0.25rem; + width: 100%; + border-color: transparent; + padding: 0.72rem; + text-align: left; + color: inherit; + background: transparent; +} + +.page-list button span { + display: -webkit-box; + overflow: hidden; + color: #716675; + font-size: 0.78rem; + font-weight: 400; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.page-list button.is-selected, +.section-list button.is-selected { + border-color: #af8bc6; + color: #3e145c; + background: #eee3f5; +} + +.section-list { + padding: 0.55rem 0.55rem 0; +} + +.section-list button { + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; +} + +.section-list + .page-list { + margin: 0.55rem; + border-top: 1px solid #ded5e3; + padding-top: 0.85rem; +} + +.navigation-empty, +.empty-message { + margin: 0; + padding: 1rem 1.2rem; + color: #716675; +} + +.page-reader { + min-width: 0; + padding: clamp(1.4rem, 4vw, 3rem); + background: #fff; +} + +.page-reader--empty { + display: grid; + place-items: center; + min-height: 20rem; + color: #716675; + text-align: center; +} + +.page-reader--error { + border-left: 0.25rem solid #b65252; +} + +.page-reader__header { + margin-bottom: 2rem; + padding-bottom: 1.25rem; + border-bottom: 1px solid #e1d9e5; +} + +.page-reader__header h2 { + margin: 0.3rem 0 0; + font-size: clamp(1.55rem, 4vw, 2.4rem); + line-height: 1.12; +} + +.page-metadata { + display: flex; + flex-wrap: wrap; + gap: 0.3rem 1rem; + margin: 1rem 0 0; + color: #716675; + font-size: 0.82rem; +} + +.page-metadata dt { + font-weight: 700; +} + +.page-metadata dd { + margin: 0 1rem 0 0; +} + +.page-content { + max-width: 48rem; + line-height: 1.65; +} + +.extracted-text { + margin: 0 0 1em; + white-space: pre-wrap; +} + +.unsupported-block { + display: grid; + gap: 0.2rem; + margin: 0.8rem 0; + border-left: 0.2rem solid #9872ae; + padding: 0.65rem 0.85rem; + color: #5e5362; + background: #f5eff8; +} + +.package-inspector, +.diagnostics { + padding: 1.25rem; +} + +.panel-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.panel-heading h2 { + margin: 0.15rem 0 0; + font-size: 1.2rem; +} + +.count-badge { + display: inline-grid; + min-width: 2rem; + min-height: 2rem; + place-items: center; + border-radius: 999px; + color: #552477; + background: #eee3f5; + font-weight: 700; +} + +.package-summary { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.7rem; + margin: 1rem 0; +} + +.package-summary > div { + border-radius: 0.65rem; + padding: 0.75rem; + background: #f6f1f8; +} + +.package-summary dt { + color: #716675; + font-size: 0.75rem; +} + +.package-summary dd { + margin: 0.2rem 0 0; + font-size: 1.1rem; + font-weight: 700; +} + +.entry-inspector { + border-top: 1px solid #ded5e3; + padding-top: 0.9rem; +} + +.entry-inspector summary { + width: fit-content; + color: #5b2c83; + cursor: pointer; + font-weight: 700; +} + +.entry-table-wrap { + max-height: 28rem; + margin-top: 0.85rem; + overflow: auto; +} + +.entry-table { + width: 100%; + border-collapse: collapse; + font-size: 0.82rem; + text-align: left; +} + +.entry-table th, +.entry-table td { + padding: 0.62rem; + border-bottom: 1px solid #e5dee9; + vertical-align: top; +} + +.entry-table th { + position: sticky; + top: 0; + background: #fff; +} + +.entry-table small { + display: block; + margin-top: 0.3rem; + color: #716675; +} + +.status { + width: fit-content; + border-radius: 999px; + padding: 0.18rem 0.46rem; + color: #655769; + background: #ebe7ed; + font-size: 0.68rem; + font-weight: 750; + text-transform: uppercase; +} + +.status--parsed { + color: #1c6645; + background: #daf0e4; +} + +.status--failed { + color: #872d2d; + background: #f6dddd; +} + +.status--unsupported { + color: #77510d; + background: #f6e9c8; +} + +.diagnostic-list { + display: grid; + gap: 0.65rem; + margin-top: 1rem; +} + +.diagnostic { + border-left: 0.22rem solid #b18124; + padding: 0.7rem 0.85rem; + background: #fbf4e6; +} + +.diagnostic--error { + border-left-color: #b65252; + background: #faeaea; +} + +.diagnostic__summary { + display: flex; + justify-content: space-between; + gap: 1rem; +} + +.diagnostic__summary span { + font-size: 0.72rem; + text-transform: uppercase; +} + +.diagnostic p { + margin: 0.35rem 0 0; +} + +.diagnostic__context { + display: grid; + grid-template-columns: max-content 1fr; + gap: 0.15rem 0.65rem; + margin: 0.65rem 0 0; + font-size: 0.76rem; +} + +.diagnostic__context dt { + font-weight: 700; +} + +.diagnostic__context dd { + margin: 0; +} + +.privacy-note { + border-left: 0.25rem solid #7d4aa0; + padding: 0.85rem 1rem; + color: #4c4054; + background: #eee7f3; +} + +.dialog-backdrop { + position: fixed; + z-index: 10; + inset: 0; + display: grid; + place-items: center; + padding: 1rem; + background: rgb(25 14 31 / 58%); +} + +.help-dialog { + width: min(32rem, 100%); + border-radius: 0.8rem; + padding: 1.5rem; + color: #241b2b; + background: #fff; + box-shadow: 0 1rem 3rem rgb(0 0 0 / 25%); +} + +.help-dialog h2 { + margin-top: 0; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; + padding: 0; +} + +@media (max-width: 760px) { + .application { + width: min(100% - 1rem, 76rem); + margin: 0.75rem auto; + } + + .reader-workspace { + grid-template-columns: 1fr; + } + + .source-navigation { + max-height: 20rem; + border-right: 0; + border-bottom: 1px solid #d9cedf; + } + + .package-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .entry-table { + min-width: 42rem; + } +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f6effa; + background: #1f1923; + } + + .drop-zone, + .status-card, + .source-toolbar, + .package-inspector, + .diagnostics, + .reader-workspace, + .page-reader, + .help-dialog, + .entry-table th { + color: #f6effa; + background: #2b2231; + } + + .source-navigation, + .navigation-heading { + background: #251d2a; + } + + .drop-zone--active, + button:hover:not(:disabled), + .page-list button.is-selected, + .section-list button.is-selected { + color: #f6effa; + background: #3c2b48; + } + + button { + color: #f4e7fc; + background: #35283d; + } + + code, + .privacy-note, + .package-summary > div { + color: #eadff0; + background: #3a3040; + } + + .drop-zone__hint, + .navigation-heading > p:last-child, + .navigation-empty, + .empty-message, + .page-metadata, + .page-list button span, + .package-summary dt, + .entry-table small { + color: #cfc2d6; + } + + .unsupported-block { + color: #e4dbe9; + background: #3a3040; + } + + .diagnostic { + color: #f5e8cd; + background: #493b23; + } + + .diagnostic--error { + color: #f8dfdf; + background: #4d2929; + } +} diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..0d74b73 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom/vitest'; +import { cleanup } from '@testing-library/react'; +import { afterEach } from 'vitest'; + +afterEach(() => { + cleanup(); +}); diff --git a/src/toolbox/manifest.definition.json b/src/toolbox/manifest.definition.json new file mode 100644 index 0000000..3a41d8f --- /dev/null +++ b/src/toolbox/manifest.definition.json @@ -0,0 +1,30 @@ +{ + "id": "de.add-ideas.onenote-tools", + "name": "OneNote Tools", + "description": "Local-first reader for tested desktop revision-store .one sections and LZX .onepkg packages; other OneNote formats are not implemented.", + "entry": "./", + "icon": "./favicon.svg", + "categories": ["documents", "onenote"], + "tags": ["local", "read-only", "one", "onepkg", "cab", "lzx"], + "integration": { + "contextVersion": 1, + "launchModes": ["navigate", "new-tab"], + "embedding": "unsupported" + }, + "requirements": { + "secureContext": false, + "workers": true, + "indexedDb": false, + "crossOriginIsolated": false + }, + "privacy": { + "processing": "local", + "fileUploads": false, + "telemetry": false, + "label": "Local only · no uploads · no storage · no telemetry" + }, + "source": { + "repository": "https://git.add-ideas.de/zemion/onenote-tools", + "license": "MPL-2.0" + } +} diff --git a/src/toolbox/manifest.test.ts b/src/toolbox/manifest.test.ts new file mode 100644 index 0000000..4b6c09b --- /dev/null +++ b/src/toolbox/manifest.test.ts @@ -0,0 +1,22 @@ +import { parseToolboxApp } from '@add-ideas/toolbox-contract'; +import { describe, expect, it } from 'vitest'; + +import generatedManifest from '../../public/toolbox-app.json'; +import { toolboxApp } from './manifest.js'; + +describe('toolbox manifest', () => { + it('matches the generated and validated public manifest', () => { + expect(toolboxApp).toEqual(parseToolboxApp(generatedManifest)); + }); + + it('does not advertise unimplemented format support', () => { + expect(toolboxApp.description).toContain('not implemented'); + expect(toolboxApp.privacy).toEqual( + expect.objectContaining({ + processing: 'local', + fileUploads: false, + telemetry: false, + }) + ); + }); +}); diff --git a/src/toolbox/manifest.ts b/src/toolbox/manifest.ts new file mode 100644 index 0000000..57da3aa --- /dev/null +++ b/src/toolbox/manifest.ts @@ -0,0 +1,12 @@ +import { defineToolboxApp, parseToolboxApp } from '@add-ideas/toolbox-contract'; + +import definition from './manifest.definition.json'; +import { APP_VERSION } from '../version.js'; + +export const toolboxApp = defineToolboxApp( + parseToolboxApp({ + schemaVersion: 1, + ...definition, + version: APP_VERSION, + }) +); diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..49a6ced --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export const APP_VERSION = '0.1.0'; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/worker/onenote.client.test.ts b/src/worker/onenote.client.test.ts new file mode 100644 index 0000000..b674bd4 --- /dev/null +++ b/src/worker/onenote.client.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it } from 'vitest'; + +import { + OneNoteWorkerClient, + WorkerClientError, + type WorkerPort, +} from './onenote.client.js'; +import type { WorkerRequest, WorkerResponse } from './worker-protocol.js'; + +class FakeWorker implements WorkerPort { + onmessage: ((event: MessageEvent) => void) | null = null; + onerror: ((event: ErrorEvent) => void) | null = null; + lastMessage: WorkerRequest | null = null; + lastTransfer: Transferable[] = []; + terminated = false; + + postMessage(message: WorkerRequest, transfer: Transferable[] = []): void { + this.lastMessage = message; + this.lastTransfer = transfer; + } + + terminate(): void { + this.terminated = true; + } + + respond(response: WorkerResponse): void { + this.onmessage?.(new MessageEvent('message', { data: response })); + } +} + +describe('OneNoteWorkerClient', () => { + it('uses the typed parse-onepkg protocol and transfers the source buffer', async () => { + const worker = new FakeWorker(); + const client = new OneNoteWorkerClient(() => worker); + const bytes = new ArrayBuffer(16); + const result = client.parseSource('onepkg', 'Notebook.onepkg', bytes); + + expect(worker.lastMessage).toEqual( + expect.objectContaining({ + type: 'parse-onepkg', + fileName: 'Notebook.onepkg', + bytes, + }) + ); + expect(worker.lastTransfer).toEqual([bytes]); + + const requestId = + worker.lastMessage && 'requestId' in worker.lastMessage + ? worker.lastMessage.requestId + : undefined; + expect(requestId).toBeDefined(); + worker.respond({ + type: 'failure', + requestId: requestId ?? 'missing', + error: { + code: 'unsupported-format', + message: 'Parser pending.', + recoverable: false, + }, + }); + + await expect(result).resolves.toEqual( + expect.objectContaining({ type: 'failure' }) + ); + }); + + it('requests a page from the retained parser session', async () => { + const worker = new FakeWorker(); + const client = new OneNoteWorkerClient(() => worker); + const result = client.getPage('session-1', 'page-7'); + + expect(worker.lastMessage).toEqual({ + type: 'get-page', + requestId: 'page-1', + sessionId: 'session-1', + pageId: 'page-7', + }); + + worker.respond({ + type: 'page', + requestId: 'page-1', + sessionId: 'session-1', + page: { + id: 'page-7', + title: 'Page seven', + text: 'Body', + textPreview: 'Body', + blocks: [{ kind: 'text', text: 'Body' }], + diagnostics: [], + }, + }); + + await expect(result).resolves.toEqual( + expect.objectContaining({ type: 'page' }) + ); + }); + + it('rejects outstanding work when the worker is terminated', async () => { + const worker = new FakeWorker(); + const client = new OneNoteWorkerClient(() => worker); + const result = client.parseSource('one', 'Section.one', new ArrayBuffer(8)); + + client.terminate(); + + await expect(result).rejects.toEqual( + expect.objectContaining>({ + code: 'worker-failure', + }) + ); + expect(worker.terminated).toBe(true); + }); +}); diff --git a/src/worker/onenote.client.ts b/src/worker/onenote.client.ts new file mode 100644 index 0000000..e620dea --- /dev/null +++ b/src/worker/onenote.client.ts @@ -0,0 +1,137 @@ +import type { + PageWorkerResponse, + ParseWorkerRequest, + ParseWorkerResponse, + WorkerRequest, + WorkerFailure, + WorkerResponse, +} from './worker-protocol.js'; + +export type SourceKind = 'one' | 'onepkg'; + +export interface WorkerPort { + onmessage: ((event: MessageEvent) => void) | null; + onerror: ((event: ErrorEvent) => void) | null; + postMessage(message: WorkerRequest, transfer?: Transferable[]): void; + terminate(): void; +} + +type PendingRequest = { + resolve: (response: ParseWorkerResponse | PageWorkerResponse) => void; + reject: (error: WorkerClientError) => void; +}; + +export class WorkerClientError extends Error { + constructor( + readonly code: WorkerFailure['code'], + message: string + ) { + super(message); + this.name = 'WorkerClientError'; + } +} + +function createModuleWorker(): WorkerPort { + return new Worker(new URL('./onenote.worker.ts', import.meta.url), { + type: 'module', + }) as WorkerPort; +} + +export class OneNoteWorkerClient { + private readonly worker: WorkerPort; + private readonly pending = new Map(); + private requestSequence = 0; + private terminated = false; + + constructor(workerFactory: () => WorkerPort = createModuleWorker) { + this.worker = workerFactory(); + this.worker.onmessage = ({ data }) => { + if (!('requestId' in data)) return; + const pending = this.pending.get(data.requestId); + if (!pending) return; + this.pending.delete(data.requestId); + pending.resolve(data); + }; + this.worker.onerror = () => { + const error = new WorkerClientError( + 'worker-failure', + 'The local parser worker stopped unexpectedly.' + ); + for (const request of this.pending.values()) request.reject(error); + this.pending.clear(); + }; + } + + parseSource( + kind: SourceKind, + fileName: string, + bytes: ArrayBuffer + ): Promise { + if (this.terminated) { + return Promise.reject( + new WorkerClientError( + 'worker-failure', + 'The local parser worker has already been terminated.' + ) + ); + } + + const requestId = `parse-${++this.requestSequence}`; + const request: ParseWorkerRequest = { + type: kind === 'one' ? 'parse-one' : 'parse-onepkg', + requestId, + fileName, + bytes, + }; + + return new Promise((resolve, reject) => { + this.pending.set(requestId, { + resolve: (response) => resolve(response as ParseWorkerResponse), + reject, + }); + this.worker.postMessage(request, [bytes]); + }); + } + + getPage( + sessionId: string, + pageId: string, + sectionId?: string + ): Promise { + if (this.terminated) { + return Promise.reject( + new WorkerClientError( + 'worker-failure', + 'The local parser worker has already been terminated.' + ) + ); + } + + const requestId = `page-${++this.requestSequence}`; + return new Promise((resolve, reject) => { + this.pending.set(requestId, { + resolve: (response) => resolve(response as PageWorkerResponse), + reject, + }); + this.worker.postMessage({ + type: 'get-page', + requestId, + sessionId, + pageId, + ...(sectionId === undefined ? {} : { sectionId }), + }); + }); + } + + terminate(): void { + if (this.terminated) return; + this.terminated = true; + this.worker.terminate(); + const error = new WorkerClientError( + 'worker-failure', + 'The local parser request was cancelled.' + ); + for (const request of this.pending.values()) request.reject(error); + this.pending.clear(); + } +} diff --git a/src/worker/onenote.worker.ts b/src/worker/onenote.worker.ts new file mode 100644 index 0000000..249a289 --- /dev/null +++ b/src/worker/onenote.worker.ts @@ -0,0 +1,122 @@ +import type { OneNotePageDto } from '../onenote/model/dto.js'; +import { + failureFromUnknown, + openPackageForWorker, + openSectionForWorker, + workerPageKey, +} from './parser-adapter.js'; +import type { WorkerRequest, WorkerResponse } from './worker-protocol.js'; + +interface ModuleWorkerScope { + onmessage: ((event: MessageEvent) => void) | null; + postMessage(response: WorkerResponse): void; +} + +interface ParserSession { + pages: Map; +} + +const workerScope = globalThis as unknown as ModuleWorkerScope; +const sessions = new Map(); +let sessionSequence = 0; + +function createSession(pages: Map): string { + const id = `session-${++sessionSequence}`; + sessions.set(id, { pages }); + return id; +} + +async function handleRequest(data: WorkerRequest): Promise { + switch (data.type) { + case 'parse-one': { + try { + const parsed = openSectionForWorker( + new Uint8Array(data.bytes), + data.fileName + ); + const sessionId = createSession(parsed.pages); + workerScope.postMessage({ + type: 'section', + requestId: data.requestId, + sessionId, + section: parsed.section, + }); + } catch (error) { + workerScope.postMessage({ + type: 'failure', + requestId: data.requestId, + error: failureFromUnknown(error), + }); + } + return; + } + case 'parse-onepkg': + try { + const parsed = await openPackageForWorker( + new Uint8Array(data.bytes), + data.fileName + ); + const sessionId = createSession(parsed.pages); + workerScope.postMessage({ + type: 'package', + requestId: data.requestId, + sessionId, + notebook: parsed.notebook, + }); + } catch (error) { + workerScope.postMessage({ + type: 'failure', + requestId: data.requestId, + error: failureFromUnknown(error), + }); + } + return; + case 'get-page': { + const session = sessions.get(data.sessionId); + if (!session) { + workerScope.postMessage({ + type: 'failure', + requestId: data.requestId, + error: { + code: 'session-not-available', + message: 'The parser session is no longer available.', + recoverable: false, + }, + }); + return; + } + const page = session.pages.get( + workerPageKey(data.pageId, data.sectionId) + ); + if (!page) { + workerScope.postMessage({ + type: 'failure', + requestId: data.requestId, + error: { + code: 'page-not-available', + message: 'The selected page is not available in this source.', + recoverable: true, + }, + }); + return; + } + workerScope.postMessage({ + type: 'page', + requestId: data.requestId, + sessionId: data.sessionId, + page, + }); + return; + } + case 'dispose': + sessions.delete(data.sessionId); + workerScope.postMessage({ + type: 'disposed', + sessionId: data.sessionId, + }); + } +} + +workerScope.onmessage = ({ data }) => { + void handleRequest(data); +}; diff --git a/src/worker/parser-adapter.test.ts b/src/worker/parser-adapter.test.ts new file mode 100644 index 0000000..4a0e2f3 --- /dev/null +++ b/src/worker/parser-adapter.test.ts @@ -0,0 +1,71 @@ +import { readFileSync } from 'node:fs'; + +import { describe, expect, it } from 'vitest'; + +import { + failureFromUnknown, + openPackageForWorker, + openSectionForWorker, + workerPageKey, +} from './parser-adapter.js'; + +function fixtureBytes(path: string): Uint8Array { + const encoded = readFileSync(new URL(path, import.meta.url), 'utf8'); + return Uint8Array.from(Buffer.from(encoded.replaceAll(/\s/gu, ''), 'base64')); +} + +describe('worker parser adapter', () => { + it('opens a real section and retains stable page identifiers', () => { + const result = openSectionForWorker( + fixtureBytes('../../tests/fixtures/onenote_desktop.one.b64'), + 'Section.one' + ); + + expect(result.section.pages).toHaveLength(3); + const first = result.section.pages[0]!; + expect(result.pages.get(workerPageKey(first.id))).toMatchObject({ + id: first.id, + title: 'Test', + text: expect.stringContaining('This notebook should have three pages.'), + }); + }); + + it('opens a real ONEPKG and addresses page IDs without rewriting them', async () => { + const result = await openPackageForWorker( + fixtureBytes('../../tests/fixtures/joplin-test.onepkg.base64'), + 'Notebook.onepkg' + ); + + expect(result.notebook.entries).toHaveLength(7); + expect(result.notebook.sections).toHaveLength(5); + expect( + result.notebook.sections.every( + (section) => section.parseStatus === 'parsed' + ) + ).toBe(true); + expect( + result.notebook.diagnostics.map((diagnostic) => diagnostic.code) + ).toContain('PACKAGE_TOC_ORDER_NOT_INTERPRETED'); + + for (const packagedSection of result.notebook.sections) { + for (const page of packagedSection.section?.pages ?? []) { + expect( + result.pages.get(workerPageKey(page.id, packagedSection.id))?.id + ).toBe(page.id); + } + } + }); + + it('converts malformed section input into a controlled worker failure', () => { + try { + openSectionForWorker(new Uint8Array(20), 'broken.one'); + } catch (error) { + expect(failureFromUnknown(error)).toMatchObject({ + code: 'invalid-format', + recoverable: false, + }); + return; + } + throw new Error('Expected malformed input to fail'); + }); +}); diff --git a/src/worker/parser-adapter.ts b/src/worker/parser-adapter.ts new file mode 100644 index 0000000..a2a9b51 --- /dev/null +++ b/src/worker/parser-adapter.ts @@ -0,0 +1,164 @@ +import { OneNoteParserError, parseOneNoteSection } from '../onenote/index.js'; +import type { + OneNotePackageDto, + OneNotePageDto, + OneNotePageSummaryDto, + OneNoteSectionDto, +} from '../onenote/model/dto.js'; +import { OnePkgError, openOneNotePackage } from '../onenote/onepkg/index.js'; +import type { WorkerFailure } from './worker-protocol.js'; + +export interface ParsedWorkerSection { + section: OneNoteSectionDto; + pages: Map; +} + +export interface ParsedWorkerPackage { + notebook: OneNotePackageDto; + pages: Map; +} + +function pageDto(page: OneNotePageSummaryDto): OneNotePageDto { + return { + ...page, + blocks: page.text ? [{ kind: 'text', text: page.text }] : [], + diagnostics: [], + }; +} + +export function workerPageKey(pageId: string, sectionId?: string): string { + return JSON.stringify([sectionId ?? null, pageId]); +} + +export function openSectionForWorker( + bytes: Uint8Array, + sourceName: string +): ParsedWorkerSection { + const section = parseOneNoteSection(bytes, { sourceName }); + return { + section, + pages: new Map( + section.pages.map((page) => [workerPageKey(page.id), pageDto(page)]) + ), + }; +} + +export async function openPackageForWorker( + bytes: Uint8Array, + sourceName: string +): Promise { + const result = await openOneNotePackage(bytes, { + sourceName, + cancellation: { + yield: () => new Promise((resolve) => setTimeout(resolve, 0)), + }, + parseSection: ({ sourceName: sectionName, bytes: sectionBytes }) => { + try { + return { + status: 'parsed', + value: parseOneNoteSection(sectionBytes, { sourceName: sectionName }), + }; + } catch (error) { + if ( + error instanceof OneNoteParserError && + error.code === 'UNSUPPORTED_FORMAT' + ) { + return { + status: 'unsupported', + diagnostics: [ + { + severity: 'warning', + code: error.code, + message: error.message, + offset: error.offset, + structure: error.structure, + recoverable: true, + }, + ], + }; + } + throw error; + } + }, + }); + + const pages = new Map(); + const sections = [...result.package.sections] + .sort((left, right) => left.path.localeCompare(right.path)) + .map((packagedSection) => { + if (!packagedSection.section) return packagedSection; + const section = { + ...packagedSection.section, + pages: packagedSection.section.pages.map((page) => { + pages.set(workerPageKey(page.id, packagedSection.id), pageDto(page)); + return page; + }), + }; + return { ...packagedSection, section }; + }); + + return { + notebook: { + ...result.package, + sections, + diagnostics: [ + ...result.package.diagnostics, + ...(sections.length > 0 + ? [ + { + severity: 'warning' as const, + code: 'PACKAGE_TOC_ORDER_NOT_INTERPRETED', + message: + 'OneNote notebook hierarchy and TOC ordering are not interpreted yet; sections are shown in normalized package-path order.', + structure: 'OneNote package section navigation', + recoverable: true, + }, + ] + : []), + ], + }, + pages, + }; +} + +export function failureFromUnknown(error: unknown): WorkerFailure { + if (error instanceof OneNoteParserError) { + const code = + error.code === 'LIMIT_EXCEEDED' + ? 'limit-exceeded' + : error.code === 'UNSUPPORTED_FORMAT' + ? 'unsupported-format' + : 'invalid-format'; + return { + code, + message: error.message, + recoverable: false, + offset: error.offset, + structure: error.structure, + }; + } + + if (error instanceof OnePkgError) { + const parserCode = error.diagnostic.code.toLowerCase(); + return { + code: parserCode.includes('limit') + ? 'limit-exceeded' + : parserCode.includes('unsupported') + ? 'unsupported-format' + : 'invalid-format', + message: error.diagnostic.message, + recoverable: error.diagnostic.recoverable, + offset: error.diagnostic.offset, + structure: error.diagnostic.structure, + }; + } + + return { + code: 'worker-failure', + message: + error instanceof Error + ? error.message + : 'An unexpected parser worker error occurred.', + recoverable: false, + }; +} diff --git a/src/worker/worker-protocol.ts b/src/worker/worker-protocol.ts new file mode 100644 index 0000000..55b4419 --- /dev/null +++ b/src/worker/worker-protocol.ts @@ -0,0 +1,87 @@ +import type { + OneNotePackageDto, + OneNotePageDto, + OneNoteSectionDto, +} from '../onenote/model/dto.js'; + +export type WorkerRequest = + | { + type: 'parse-one'; + requestId: string; + fileName: string; + bytes: ArrayBuffer; + } + | { + type: 'parse-onepkg'; + requestId: string; + fileName: string; + bytes: ArrayBuffer; + } + | { + type: 'get-page'; + requestId: string; + sessionId: string; + pageId: string; + sectionId?: string; + } + | { + type: 'dispose'; + sessionId: string; + }; + +export interface WorkerFailure { + code: + | 'invalid-format' + | 'unsupported-format' + | 'limit-exceeded' + | 'page-not-available' + | 'session-not-available' + | 'worker-failure'; + message: string; + recoverable: boolean; + offset?: number; + structure?: string; +} + +export type WorkerResponse = + | { + type: 'section'; + requestId: string; + sessionId: string; + section: OneNoteSectionDto; + } + | { + type: 'package'; + requestId: string; + sessionId: string; + notebook: OneNotePackageDto; + } + | { + type: 'page'; + requestId: string; + sessionId: string; + page: OneNotePageDto; + } + | { + type: 'failure'; + requestId: string; + error: WorkerFailure; + } + | { + type: 'disposed'; + sessionId: string; + }; + +export type ParseWorkerRequest = Extract< + WorkerRequest, + { type: 'parse-one' | 'parse-onepkg' } +>; +export type ParseWorkerResponse = Extract< + WorkerResponse, + { type: 'section' | 'package' | 'failure' } +>; + +export type PageWorkerResponse = Extract< + WorkerResponse, + { type: 'page' | 'failure' } +>; diff --git a/tests/binary-property.test.ts b/tests/binary-property.test.ts new file mode 100644 index 0000000..48e1b9f --- /dev/null +++ b/tests/binary-property.test.ts @@ -0,0 +1,74 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import { describe, expect, it } from 'vitest'; + +import { BinaryReader } from '../src/onenote/binary/BinaryReader.js'; +import { parseObjectPropSet } from '../src/onenote/onestore/property-set.js'; +import { OneNoteParserError } from '../src/onenote/parser/error.js'; +import { resolveParserLimits } from '../src/onenote/parser/limits.js'; + +describe('bounded OneStore primitives', () => { + it('reports an absolute offset instead of reading beyond its view', () => { + const reader = new BinaryReader( + Uint8Array.from([0, 1, 2, 3, 4, 5]), + 2, + 3, + 'test view' + ); + expect(reader.u16()).toBe(0x0302); + + try { + reader.u16(); + expect.unreachable('the read should fail'); + } catch (error) { + expect(error).toBeInstanceOf(OneNoteParserError); + expect(error).toMatchObject({ + code: 'BOUNDS_EXCEEDED', + offset: 4, + structure: 'test view', + }); + } + }); + + it('decodes a bounded object property set without reading Boolean bytes', () => { + const bytes = Uint8Array.from([ + 0x00, + 0x00, + 0x00, + 0x80, // OID stream: zero IDs, OSID stream absent + 0x02, + 0x00, // two properties + 0x01, + 0x00, + 0x00, + 0x88, // Boolean true + 0x02, + 0x00, + 0x00, + 0x1c, // byte vector + 0x03, + 0x00, + 0x00, + 0x00, + 0xaa, + 0xbb, + 0xcc, + ]); + const parsed = parseObjectPropSet( + bytes, + { offset: 0, size: bytes.length, nil: false, zero: false }, + resolveParserLimits() + ); + + expect(parsed.properties.entries[0]?.value).toEqual({ + kind: 'bool', + value: true, + }); + expect(parsed.properties.entries[1]?.value).toMatchObject({ + kind: 'bytes', + value: Uint8Array.from([0xaa, 0xbb, 0xcc]), + }); + }); +}); diff --git a/tests/desktop-section.test.ts b/tests/desktop-section.test.ts new file mode 100644 index 0000000..e053cfd --- /dev/null +++ b/tests/desktop-section.test.ts @@ -0,0 +1,83 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { + OneNoteParserError, + detectOneNoteFormat, + parseOneNoteSection, +} from '../src/onenote/index.js'; + +const fixture = Buffer.from( + readFileSync( + join(process.cwd(), 'tests/fixtures/onenote_desktop.one.b64'), + 'utf8' + ).replace(/\s/g, ''), + 'base64' +); + +describe('desktop OneStore section parser', () => { + it('parses the pinned Joplin golden file into serializable page text', () => { + expect(fixture).toHaveLength(34_904); + expect(createHash('sha256').update(fixture).digest('hex')).toBe( + '7c43e85e2169b4bc3236db73a24c8f20d5c284b74563c07baf1406887d20eb5e' + ); + expect(detectOneNoteFormat(fixture).kind).toBe('desktop-one'); + + const parsed = parseOneNoteSection(fixture, { + sourceName: 'onenote_desktop.one', + }); + expect(structuredClone(parsed)).toEqual(parsed); + expect(parsed.pages).toHaveLength(3); + + expect(parsed.pages[0]).toMatchObject({ + id: '{27CDD549-7E76-45B0-B18D-5A9BCF261CCB}', + title: 'Test', + createdAt: '2025-10-17T20:59:14.797Z', + updatedAt: '2025-10-17T21:38:35.000Z', + level: 1, + }); + expect(parsed.pages[0]!.text).toContain('Test!'); + expect(parsed.pages[0]!.text).toContain('3+3=6'); + expect(parsed.pages[0]!.text).toContain( + 'This notebook should have three pages.' + ); + + expect(parsed.pages[1]).toMatchObject({ + id: '{A7185C1A-6895-48FA-B9DD-380B511D0F82}', + title: 'Another page', + createdAt: '2025-10-17T21:00:24.848Z', + updatedAt: '2025-10-17T21:00:57.000Z', + level: 1, + }); + expect(parsed.pages[1]!.text).toContain('Testing!'); + + expect(parsed.pages[2]).toMatchObject({ + id: '{1A5CF75B-BF4A-4F37-B48C-F5493F4ED828}', + title: 'Page 3', + createdAt: '2025-10-17T21:38:42.795Z', + updatedAt: '2025-10-17T21:38:49.000Z', + level: 1, + }); + expect(parsed.pages[2]!.text).toContain( + 'This page is nearly empty. It has a single text box and not much else.' + ); + + const codes = parsed.diagnostics.map(({ code }) => code); + expect(codes).toContain('UNSUPPORTED_IMAGE'); + expect(codes).toContain('UNSUPPORTED_TABLE'); + expect(codes).toContain('UNSUPPORTED_INK'); + }); + + it('fails malformed input with a controlled parser error', () => { + expect(() => parseOneNoteSection(new Uint8Array(63))).toThrowError( + OneNoteParserError + ); + }); +}); diff --git a/tests/file-node.test.ts b/tests/file-node.test.ts new file mode 100644 index 0000000..8535fc7 --- /dev/null +++ b/tests/file-node.test.ts @@ -0,0 +1,93 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +import { describe, expect, it } from 'vitest'; + +import { DesktopFileNodeParser } from '../src/onenote/onestore/file-node.js'; +import { OneNoteParserError } from '../src/onenote/parser/error.js'; +import { resolveParserLimits } from '../src/onenote/parser/limits.js'; + +const MAGIC = 0xa4567ab1f5f7f4c4n; +const FOOTER = 0x8bc215c38233ba4bn; + +describe('DesktopFileNodeParser', () => { + it('rejects a multi-fragment cycle immediately', () => { + const bytes = new Uint8Array(72); + writeFragment(bytes, 0, 0x10, 0, 36); + writeFragment(bytes, 36, 0x10, 1, 0); + + const parser = new DesktopFileNodeParser(bytes, resolveParserLimits()); + const parse = () => + parser.parseList({ offset: 0, size: 36, nil: false, zero: false }); + + expect(parse).toThrowError(OneNoteParserError); + expect(parse).toThrow(/cycle/i); + }); + + it('stops decoding at ChunkTerminatorFND and permits an opaque tail', () => { + const bytes = new Uint8Array(52); + const view = new DataView(bytes.buffer); + view.setBigUint64(0, MAGIC, true); + view.setUint32(8, 0x10, true); + view.setUint32(12, 0, true); + view.setUint32(16, 0x10ff, true); // id 0xff, declared size 4 + bytes.fill(0xa5, 20, 32); // deliberately not valid FileNode words + view.setBigUint64(32, 0xffffffffffffffffn, true); + view.setUint32(40, 0, true); + view.setBigUint64(44, FOOTER, true); + + const parser = new DesktopFileNodeParser(bytes, resolveParserLimits()); + const parsed = parser.parseList({ + offset: 0, + size: bytes.length, + nil: false, + zero: false, + }); + + expect(parsed.nodes).toEqual([]); + }); + + it('uses the transaction-log count to delimit nonzero fragment padding', () => { + const bytes = new Uint8Array(52); + const view = new DataView(bytes.buffer); + view.setBigUint64(0, MAGIC, true); + view.setUint32(8, 0x10, true); + view.setUint32(12, 0, true); + view.setUint32(16, 0x1004, true); // id 4, declared size 4 + bytes.fill(0xa5, 20, 32); // opaque tail without a terminator + view.setBigUint64(32, 0xffffffffffffffffn, true); + view.setUint32(40, 0, true); + view.setBigUint64(44, FOOTER, true); + + const parser = new DesktopFileNodeParser( + bytes, + resolveParserLimits(), + new Map([[0x10, 1]]) + ); + const parsed = parser.parseList({ + offset: 0, + size: bytes.length, + nil: false, + zero: false, + }); + + expect(parsed.nodes.map(({ id }) => id)).toEqual([0x004]); + }); +}); + +function writeFragment( + bytes: Uint8Array, + offset: number, + listId: number, + sequence: number, + nextOffset: number +): void { + const view = new DataView(bytes.buffer, offset, 36); + view.setBigUint64(0, MAGIC, true); + view.setUint32(8, listId, true); + view.setUint32(12, sequence, true); + view.setBigUint64(16, BigInt(nextOffset), true); + view.setUint32(24, 36, true); + view.setBigUint64(28, FOOTER, true); +} diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..b7db36a --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,64 @@ +# Fixture provenance + +No private notebook may be committed. Each binary fixture requires a public, +reviewed source, an immutable revision, a redistribution licence, a content +expectation, and a retained hash. Base64 encoding is only a repository transport +and does not change the underlying file's licence. + +## `onenote_desktop.one.b64` + +- Source: Joplin + [`packages/app-cli/tests/support/onenote/onenote_desktop.one`](https://github.com/laurent22/joplin/blob/28eb53bd9f0cd4ae0dec0c59a08037075a38302b/packages/app-cli/tests/support/onenote/onenote_desktop.one), + introduced at commit + [`28eb53bd9f0cd4ae0dec0c59a08037075a38302b`](https://github.com/laurent22/joplin/commit/28eb53bd9f0cd4ae0dec0c59a08037075a38302b). +- Licence: AGPL-3.0-or-later; full text in `LICENSES/AGPL-3.0.txt`. +- Decoded size: 34,904 bytes. +- Decoded SHA-256: + `7c43e85e2169b4bc3236db73a24c8f20d5c284b74563c07baf1406887d20eb5e`. +- Golden expectation: desktop OneStore section with three pages — `Test`, + `Another page`, and `Page 3` — plus readable text/timestamps and diagnostics + for image, table, and ink content. + +## `joplin-test.onepkg.base64` + +- Source: Joplin + [`packages/app-cli/tests/support/onenote/test.onepkg`](https://github.com/laurent22/joplin/blob/1e73aad7eb08fde5d9e4cb533df40052a5cd32d7/packages/app-cli/tests/support/onenote/test.onepkg) + at commit `1e73aad7eb08fde5d9e4cb533df40052a5cd32d7`, introduced by + [Joplin PR #14094](https://github.com/laurent22/joplin/pull/14094). +- Licence: AGPL-3.0-or-later; full text in `LICENSES/AGPL-3.0.txt`. +- Decoded size: 11,066 bytes. +- Decoded SHA-256: + `83cb622d40f0ab127038b629553b6926be3e2a6ce642f169df0e2614c13e8469`. +- CAB expectation: one LZX `0x1203` folder, three CFDATA blocks, seven UTF-8 + and nested paths, five `.one` sections, and two `.onetoc2` entries. +- Parser expectation: all five sections parse, producing seven pages in total: + `Page 1`, `Page 2`, `Test`, `Test page`, `Testing…`, `A test`, and + `Tuesday, January 13, 2026`. + +Tests verify the package hash, all seven paths, the native `.one` magic for +every section, section parse statuses, and representative title/text output. + +## Inline rust-cab CAB fixtures + +`src/onenote/onepkg/cabinet.test.ts` contains byte-for-byte test arrays copied +from rust-cab `src/cabinet.rs` at commit +[`c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2`](https://github.com/mdsteele/rust-cab/tree/c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2). +They are MIT-licensed; the exact upstream text is in +`LICENSES/rust-cab-MIT.txt`. + +The arrays cover an uncompressed single-entry CAB, an uncompressed two-entry +CAB, and an LZX `0x1303` CAB containing `hi.txt` and `bye.txt`. They are CAB +unit fixtures, not OneNote exports, and are not evidence of `.onepkg` producer +compatibility. + +## Distribution policy + +The decoded fixture bytes are used only by tests. Vite does not copy them into +`dist`, and the release packager does not add them to the application ZIP. The +release may include this provenance document and licence text so its notices +remain complete. + +A copied upstream fixture needs an individual licence and redistribution +review; a repository-level licence must not be assumed to cover every binary. +Generated fixtures must document the generator command/input and must not be +described as real application exports. diff --git a/tests/fixtures/joplin-test.onepkg.base64 b/tests/fixtures/joplin-test.onepkg.base64 new file mode 100644 index 0000000..f2e616d --- /dev/null +++ b/tests/fixtures/joplin-test.onepkg.base64 @@ -0,0 +1,123 @@ +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 diff --git a/tests/fixtures/onenote_desktop.one.b64 b/tests/fixtures/onenote_desktop.one.b64 new file mode 100644 index 0000000..4903ae6 --- /dev/null +++ b/tests/fixtures/onenote_desktop.one.b64 @@ -0,0 +1,613 @@ +5FJce4zYp02usVN40CmW04I9Xd2xTcNFr4sU1DnPIloAAAAAAAAAAAAAAAAAAAAAP92aEBuR9Uml +0BeR7ciu2CoAAAAqAAAAKgAAACoAAAAAAAAAAAAAAP////8AAAAAHgAAAAAAAAAAAAAAAAAAAP// +//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIhIAAAAAAAAAAQAAAAIAAAAAAAAaAkA +AAAEAAAAAAAAAAQAAP//////////AAAAAFiIAAAAAAAASAAAAAAAAAB7imxM14TUSIBpD9JgoQCk +WwAAAAAAAACa8y+bI/UGRKDsaWzPa/jiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPax8Dr2 +sfA69rHwOvax8DoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMT0 +9/WxelakEAAAAAAAAAAIbACVLQIkSkB3GrP0i0aXbGBF41TEagEAAAAEYICASkB3GrP0i0aXbGBF +41TEagEAAAAIbACV/gIkcRzRR+FsFUWTEWg2Fvz2TwEAAAAIbACVrAgkdC0xMs9rdUia3OOLQTpX +fwEAAACQHACVPQ4kCGwAlaUMJIvdQh+UOmxHiZC4rvZt8pkBAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////////8AAAAAS7ozgsMVwosQAAAA +AAAAAAEAAABC7pkZEAAAAAIAAAARAAAAAQAAAAEAAAAGlT0TEQAAAAIAAAASAAAAAQAAAAEAAADd +4WE1EgAAAAcAAAATAAAAFQAAAAEAAADBZTL0EAAAAAMAAAAUAAAAAQAAAAEAAAAQvIY3FAAAAAIA +AAAVAAAAAQAAAAEAAABwN9uZFgAAAAAAAAABAAAAvTaj7BYAAAABAAAAAQAAAEv077gWAAAAAgAA +AAEAAADRRJgNFgAAAAMAAAABAAAA+3b2gxYAAAAEAAAAAQAAAAdC/2cWAAAABQAAAAEAAABMZqw/ +FgAAAAYAAAABAAAAzHT7vxYAAAAHAAAAAQAAAF0bMv4WAAAACAAAAAEAAABR/sfRFgAAAAkAAAAB +AAAA7qWTcBUAAAAIAAAAFwAAAF8AAAABAAAAaFocHhUAAAAOAAAAGAAAAAgAAAABAAAAP+VA/xAA +AAAEAAAAGQAAAAEAAAABAAAA+kgRnhkAAAACAAAAGgAAAAEAAAABAAAAYsgZyhAAAAAFAAAAGwAA +AAEAAAABAAAAI0YX8hYAAAAKAAAAAQAAAFGz0RQWAAAACwAAAAEAAAAh9+u3GgAAAAgAAAAcAAAA +JwAAAAEAAABgw5DuGgAAAA4AAAAdAAAACAAAAAEAAACNjHtsEAAAAAYAAAAeAAAAAQAAAAEAAABS +0ZDMHgAAAAIAAAAfAAAAAQAAAAEAAABQ657oFgAAAAwAAAABAAAAfsnmFB8AAAAIAAAAIAAAAB4A +AAABAAAAGXxljx8AAAAOAAAAIQAAAAgAAAABAAAALMo38QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAMT09/WxelakEQAAAAAAAAAMYICASkB3GrP0i0aXbGBF41TEagEAAAAQHACVUQIk +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +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 +1ALoBewFqBuEBfQCjAFYgIAB4hXiFeIV4hXiFeIV4hXiFeIV2EWCEP8N/Q3/Dd0TtXrLQ/sdvgG8 +IpsW3wcBAAAACQQAABAAAABc/6CpmmvPRanFqSKD+WlyCAAAAHRa2+moP9wBAAABAACAIwAAAA4A +eh0AFBQcABQVHAAUFTQAIE4dABSkNQAUnjQAFJ80ABSgNAAUoTQAFKI0ABSjNAAUpDQAFKU0ABQr +CCRWAAAAAAAAAAABAAAAAQAAADs39T+oupNASvIFQDD95D58guk/eM2QQAmnEUCUUSE/AAABAACA +JAAAAAEAFjQAJAEAAAAAAAAAAAABAACAHwAAAAgACzQAHBk0ABQbNAAQHDQADBA2AAwaNAAcHTQA +HAk0ACBkAAAAigGEJoAD5AXcBawLbGxsaGhk6AFY7AEwNDQ0NIgBXPQBWLxkAAARhQEVGR0hJSl1 +OakBKSUlISFBKYEBOYCAAcapAbqAAcMJ34IBqRapFqsWqRapFqkWqxZjYwAAAAAAAAAAAAIAAAAJ +BAAAEAAAAGUrZHn/jZxJgRnONItyJ7wIAAAAmdhM6qg/3AEAAAAAAAABAACAJwAAAA4Aeh0AFBQc +ABQVHAAUFTQAIE4dABSkNQAUnjQAFJ80ABSgNAAUoTQAFKI0ABSjNAAUpDQAFKU0ABQuCCRWAAAA +AAAAAAABAAAAAQAAAJ6bOkDqdKpAP4aJQLqQm0A/wTRAuoenQJ5gj0AZa6FAAAABAACAKAAAAAEA +FjQAJAEAAAAAAAAAAAABAACAHwAAAAgACzQAHBk0ABQbNAAQHDQADBA2AAwaNAAcHTQAHAk0ACAy +DQAA1izwOQAEAAQABAQEBAQEBAgQeBwwNDg4ODg8nAGsAZgDgAIgIBwgIBwgHBwgHBwYHBgcGBQY +FBgQFBAQEBAYBAQABAAEAAAAAAAAAAAFAAUABQAFBQUFBQkNCQ0NDQkNDQ0NEQ0lVVVVVVG9AQ05 +OTU1NTUxMS0xVSEhHR0ZGRkVFRERDQ0RDQkFBCAEAAgEBAgICAgMDAgMEAwMJBgcHBwcHBwcIFBs +yAKUAawCwAG8AcwDZGRgYFxY4AGABWgwMGQQEAwIDAgICAgAAAAAAAAABQAJAAAAAAUAAAAFAAAF +AAAFAAAFAAAFAAAFAAUACQ0JCQkJCQUJCQkFDUE5NTUxWSkpRREJCQUFCAQABQAFAAUFAAUFBQUF +BQUFCQUFBQkFCQUJBQkFCQUJERERDRERERENERENKQAFAAAAAAAFAAAAAAAABQAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAQsDJQFjAGEAdgCoAKIBcQCdHRo8AFIQIABLFAgDAkV +NVWlAZUB/QbRBf0ItQLxBCFdDQkJBQUFAAAEAAAAAAQABAAEAAQEAAQIBAQEBAQEBAQEBAQIBAQE +BAwYHBgYGDQEcOgHkAGQAYgBhAG0Ajw8iAFYAAQAAAQAAAQABAQABAAEBAAIAAAEAAAEAAAABAAA +AAQAAAAABAAAAAAAAAAAAAgABAAFCQ0NRd0BeRk1CQUFBQAFAAAAAAAAAAAAAAAEAAAAAAQAAAAE +AAAEAAAEAAAEAAQABAAABAAEAAQABFSQAfwC7AOQAmywAwQIBAgICAQICAgIGAAEAAQABAAABAAE +AAAEAAAEAAAEAAAQDQUFBQAAAAQAAAAEAAAEAAQEAAQEBAQEAAQIBAQEBAQECAQEBAgEBAgMHBwg +HBxILMgDDAwQDAwQDBAMEAwMKAgMCAgIBAgICAgQMAwMCAgICAQECBwIBAgICAgICAwIDAwMDBwc +HCAcICAgICAgIFw8ODw4OJQBLDAsLCwoJFA8PDhoICAcHBQUECAIBBElBckD6QHJAxERDQ0JDREA +AAUAAAAAAAAAAAAABAAAAAQAAAQABAAEAAQABAQABAQABAQEAAQEBAAIFBQYFBQUFBQYFDRMrAEw +eHh0cGhogAJgzAEoAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAEAAAAAAAAAAAABAAAAAAA +AAAAAAAABAAAAAAAAAURFRUZRU3VBMUClQSFAUGdAT1JCQkFCQkNCQkNCQ0NDQkNDSEJCQkNCQkJ +DQkJCQkJDQkJBQkJCQUJCQ0RERENEQkNCQkJBQkFBQAUCAwQEBAUGBgcSFxc2H0IDAgMCAgMCAgI +DAgIFHQULCwoJCQgHEA0ZCQAAAUABQkJCQkNDQ0RERERFRUVGRkZGRkdGR1FBQkFCQkFCQkFCQkF +CQkFCQkFCQUJBQkFCQkJCQkFCQUFBQUFAAUAAAQIDBA0BBAQFBgYHCAgICRQICAkJCQoKCgoLCws +LFyAAYABgAF8pAIUEBQQFBAUEBAQEBAQDBAMHBQMEAwMDAgICAwMAAktKTWdATExOTlBQckBzQV9 +PUGdAR0dHR0dIR0hVQkFCQUJBQkFCREABQAABQAAAAUAAAAFAAAAAAAABQAAAAAAAAAEBAQEBAQE +BAgECAQQREhIUEyoAVxg3AFESEhISJgBqAIECAQIBAgECAQECAQEBAQIBAAEBAQEAAQEAAAEAAAA +AAQAAAAABQAFBQUFFQAAAAAAAAAAAAAABQAAAAAABQAAAAAABQAAAAAABQAAAAAFAAAAAAAFAAAA +AAAABQAAAAAZCd0CVVmVApkCtQXhAoUBjQGNAfkChQGJAckCkQHtAtkBfSUEOHigAoQC6ArMCJwP +0AS0C1SYAigkKCRsCAQEBAQIBAQEBAQECAQEBAQECAQABAAABAAABAAAAAAAAAAFCQkJDQ0dBUHl +BGltdX3NAlVd+QGpAQUABQUABQUABQUABQAFAAAFBQAEAAAAAAAABAAAAAAEAAAABAAAAAQAAAQA +AAQUFBRAUFRQVOwBqASoAjSQARAQEBAQNAAABAAEAAQAAAQABAAEAAAEAAAEAAAEAAAEAAAAAAQA +AAAAAAAAAAAAAAAhTfkBgQPdAVXJAgUFBQUFBQUFBQAFBQAAAAAEAAAAAAAEAAAABAAABAAABARA +uAEwNIABBAQIBAQIBAQIBAQEBAgEBAQEBAQEBAAEBAQABAAEAAAEAAAAAAAFBQUJCR0RwQEFBQUF +BQUABQAAAAAIBAAEBAQEBAQIBBRIICQkJCRUJCgkbBAMDAwQDAwIDAwIDAgIEBAMDAgICAgEBAAA +BQkNDREROR0ZIR0lISlRTU1NsQE9QUFFRUVFvQFJWQwsBIgF2AKwBRwQFBQUFCwEBAAEBAQABAQE +BAAEBAQABAQEAAQEAAQEAAQABAAEAAAEAAAEAAAAAAAABAAEAAUABQUFBRkhXRlJUVVZXWWdAnm1 +Aj0AAAAAAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAA +AAAAMDxAPECMAZwBwAuACIQM2AK0AYwDfHwMDAwIDAgMCAgICAgECAQICAAABAAFAAAFAAUABQUF +BQUFCQUJBQkFFRkZGR0ZHR0dHR0dRUVBRcEBNTk1NTE1MTExbX15gIABvg3ADcANvg3ADb4NwA3A +Db4NwA2+DcANwA2+DcpYjgSNAY0BjQGNAY0BjQGNAY0B2y3dLeAFsAOwA64DsAOwA7ADsAOwA64D +sAOwA7ADsAOuA7ADsAOwA7ADrgOwA7ADsAOwA64DsAOwA7ADsAMAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlQGVAZUBkwGVAZUBlQGVAZMBlQGVAQsLCwkLCwsL +CwsLCwsL0wbTBtUG0wbVBvUB9QH1AfMB9QH1AfUB9QH1AfUB9QH1AfUB9QH1AfUB8wHFBcMFxQXD +BcUFwwXFBcMFxQXDBZ9An0CpDacN2ALaAtoCAAAAAAAAAAAAIyEhhgGIAYYBiAGIAYYBiAGGAYgB ++gL6AvoC+gL6AvgC+gL6AvoC+gIcHhwcHhwcHhwcHhwcHhwcHhwcHhwcHhwcHhytAq8CrQKvAq0C +rQKvAq0CrQKvAq0CrwKtAuEC3wLhAt8C4QLhAuYN5g3oDcgcyBzIHMgcyhzIHMJHAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABr0wTVBNMExR+0Eps0qSqrKqkqqyqvCq8K +rwp9fdwvgLMBuBCxBQ2+FL4U3AKgAR8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAC2/GLEIsQizCLEIsQj9LP0s/yzyEdQD0gPSA9QD0gPSA9ID1APS +A9ID1APSA9ID0gPUA9ID0gPUA44CjgKOAo4CjgKOAo4CjgKOAo4CjgKOAo4CjgKOAo4CjgKOAo4C +jgKOAowCjgKOAo4CjgKOAo4C4g7kDuIO4g71A/UD8wP1A/UD+xlitAvVHpcblRuVG5cblRuVGyMh +IyEhIyEhIyEhIyEhIyEhIyEjISEjISEjISEjISEjISEjISEjISMhISOEAesW6Rb2Av8BAADIAcoB +yAHIAcoByAHKAcgByAHKAcgBygGgAZ4BoAGgAaABoAGgAaABngGgAaABoAGgAaABoAGgAZ4BoAGg +AaABoAGgAeIV3mn4I/gj+CMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAC/EBoYGhoYGhoYGhoYGhgwLi4uLi4uLi4uLv0rW1lbW1tb9Aj0CPQI9AjAAcABwAHAAcAB +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 +5AbwAoAG1Am4AbwBuAG4AYADsAGUBKwCdHh4lALgAdgCGBgYGBgYFBgUGBQUFDgoKCgkXDw4NDBM +OIACdEg8LCwEDBRI9AFgcJQC+AOoAcwD8AKYAegBwAIwJCAUEAQEKGh0JLAB7AGIArgEIBgQAAUF +BQUJCQkJDQ0pFRUVGRkZGR0dRYEBvQaBBZ0HjQW5Fc0P7Qn5LOUD1QrdEA2BBgkJCQkJBQkJCQkJ +EQAFBQUFBQAFBQAFAAUABQAABQAAAAUAAAAABAgoIHikAZgBpATsAjQYUX11tQJRHSUJAAAFCQ0p +ba0BuQE9ZZEC+QIxLUkJJQkNDQ01HcyZAUpISkhKSEpISkhKSEpISEpISkhKSEpISkhKSEpIoguI +B4gHhgeIB4YHiAeGB/Ykigaqc+YC5gLmAuYC5ALCBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAABzc3Nzc3Nzc3NzcXOrAqkCqwKpAqsCqQKrAqkCqwKrAuEWnVuzT4MeogEAAAAAAAAA +AAAuLi4uLi4uLi4uLi6iAaABogGgAaIBoAGiAaABoAGiAaABogGgAaIBoAGiAaABogGgAaIBoAGi +AaABogGsFKwUrBSsFKwU5h7oSZYPAAAAALEKlSOrf71BxAGeaciDAQAAT09NT09NzTHLMft69xPX +B5EDPMINwA3CDfYEqhXKA8wDygPMA8oDXAoAAAAJBAAAEAAAAKYNIISyTPZOjm8QHx5xGD4IAAAA +uJUW9Kg/3AEAAAAAAAEAAIBgAAAADgB6HQAUFBwAFBUcABQVNAAgTh0AFKQ1ABSeNAAUnzQAFKA0 +ABShNAAUojQAFKM0ABSkNAAUpTQAFD8IJFYAAAAAAAAAAAEAAAABAAAA/P0mQc9NbUAAYM46AGjO +OmSHJUFwc2dArug8Pr7oPD4AAAEAAIBhAAAAAQAWNAAkAQAAAAAAAAAAAAEAAIBcAAAACAALNAAc +GTQAFBs0ABAcNAAMEDYADBo0ABwdNAAcCTQAIAkAAAAGkM8BzEnMmQELAAAACQQAABAAAADLh36c +DrrqSrdm63cJyBSnCAAAAPKsQvaoP9wBAAAAAIACANg1ABxUNgAUFAAAAAAAAAAAAAAAAAAAAAAA +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 +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAA//////////8AAAAAS7ozgsMVwosAAACAAwD4HQAcdR0AHDAe +ABwGAAAASABIAAAAGAAAAEgAZQBuAHIAeQAgAEgAZQBpAG4AbwAAAAIAAAAAAAAAAAAAgA4AWjQA +HAQcAAgFHAAIBhwACAwcABQuNAAULzQAFDA0ABQHHAAIDRwAFAkcAAgIHAAIChwAHAscABAUAAAA +UABhAGcAZQBUAGkAdABsAGUAAAAAAAD/AAAAAAAAAAAAAAAAAAAA/xwAAABDAGEAbABpAGIAcgBp +ACAATABpAGcAaAB0AAAAKAAAAAAAAAAAgA4AWjQAHAQcAAgFHAAIBhwACAwcABQuNAAULzQAFDA0 +ABQHHAAIDRwAFAkcAAgIHAAIChwAHAscABAaAAAAUABhAGcAZQBEAGEAdABlAFQAaQBtAGUAAAB2 +dnYAAAAAAAAAAAAAAAAAAAAA/xAAAABDAGEAbABpAGIAcgBpAAAAFAAAAAAAAIACAPgdABx1HQAc +BgAAAEgASAAAABgAAABIAGUAbgByAHkAIABIAGUAaQBuAG8AAAAAAAAAAAAAgAoAFh4ACKo0AAgU +HgAIGR4ACDscABQBHQAMUjQADAE0AAgFNAAIBjQACAkEAAAAAAAAAAAAAACABAAKNAAcDzQAFAw0 +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 +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAD//////////wAAAABLujOCwxXCiwEAAEAMAAAAAAAAQAEAAAABAQAAAgAfHAAkezQANAEAAAAB +AAAAAAAAAACABgDzHAAcMBwAHP8dABSCHQAUizQAFGUcABgaAAAAQQBuAG8AdABoAGUAcgAgAHAA +YQBnAGUAAAAQAAAAGlwYp5Vo+ki53TgLUR0PggEAAAAoAAAAKAAAAAAJCA+pP9wBAAAAAAMAAIAk +AAAAMwAAAA0AAAAUAHodABQgHAAkXx0AJNwcAIiOHACITBwAFE0cABROHAAUTxwAFAEcABQCHAAU +nhwAFJ8cABSgHAAUoRwAFHUdABz4HQAckhwACA8dABQQHQAUiQgkVgIAAAABAAAAAACAPwAAgD8A +AABAAAAAQFCNp0EAAK5BAAAAPwAAAD8AAIA+AACAPhgAAABIAGUAbgByAHkAIABIAGUAaQBuAG8A +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 +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAD//////////wAAAABLujOCwxXCi8T09/WxelakHwAAAAAAAAAUcICAi91CH5Q6bEeJkLiu +9m3ymQEAAAAAAAAAHsiAgA8SC0IBwxlNuHjyksa4SfsBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB +AAAAAACwbACVNxBUF5e/HFAwoQ4Ww24n8DB/JgAAAACEZACM//////////8AAAAAAAAAAABv0i+x +WnCAgDqBEp0wektBri663LwAQOUKAAAAAQAAAFpwgIA6gRKdMHpLQa4uuty8AEDlCwAAAAIAAABa +cICAOoESnTB6S0GuLrrcvABA5RoAAAAEAAAAHBCAgP8QAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAABYhAAAAAAAAAAEAABLujOCwxXCiwEAAEAMAAAAAAAAQAEAAAABAQAAAgAfHAAkezQA +NAEAAAABAAAAAAAAAACABgDzHAAcMBwAHP8dABSCHQAUizQAFGUcABgOAAAAUABhAGcAZQAgADMA +AAAQAAAAW/dcGkq/N0+0jPVJP07YKAEAAAAoAAAAKAAAALDrtmiuP9wBAgAAgB0AAAANAAAAFAB6 +HQAUIBwAJF8dACTcHACIjhwAiEwcABRNHAAUThwAFE8cABQBHAAUAhwAFJ4cABSfHAAUoBwAFKEc +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 +AAEBpEQAja4PDw4AAAAMAAYAAQGkRACN/g8OGQAAAA4ABgABAcSEAI28CQcbAAAAAQASAAABex3p +sZAjgkiiuKJqQHRv+qREAI19DwUKAAAANwAGAAMBuBCAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAD//////////wAAAABLujOCwxXCi8T09/WxelakHwAAAAEAAAAfGIGA5Db36JlU3k2DJGpS8YLu +AQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAH9JEXFrGwlClJHJiwTPTFoBAAAAsGwAldAM +FPyzQ7bIp2YOLZ/258cK2NwAAAAAhGQAjP//////////AAAAAAAAAAAAkrg0EVpwgIB/SRFxaxsJ +QpSRyYsEz0xaAgAAAAEAAABacICAf0kRcWsbCUKUkcmLBM9MWgMAAAACAAAAHBCAgAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +//////////8AAAAAS7ozgsMVwos= diff --git a/tests/golden/README.md b/tests/golden/README.md new file mode 100644 index 0000000..e5b5bd3 --- /dev/null +++ b/tests/golden/README.md @@ -0,0 +1,5 @@ +# Golden parser results + +No golden results exist in Phase 0. Future checked-in normalized results must +identify their fixture and reference-oracle revision. Normal tests must not +require Rust. diff --git a/tests/release-archive.test.ts b/tests/release-archive.test.ts new file mode 100644 index 0000000..86eaa50 --- /dev/null +++ b/tests/release-archive.test.ts @@ -0,0 +1,121 @@ +import { createHash } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +import { + collectDirectoryFiles, + createThirdPartyLicenseBundle, +} from '../scripts/release-archive.mjs'; + +interface PackageReleaseModule { + collectReleaseMetadataFiles( + projectRoot: string + ): Promise>; +} + +describe('release archive', () => { + it('creates identical bytes in different process timezones', () => { + const hashes = ['UTC', 'Europe/Berlin', 'America/New_York'].map( + (timezone) => { + const script = ` + import { createHash } from 'node:crypto'; + import { createDeterministicZip } from './scripts/release-archive.mjs'; + const archive = createDeterministicZip({ + 'z.txt': Buffer.from('last'), + 'a.txt': Buffer.from('first'), + }); + process.stdout.write(createHash('sha256').update(archive).digest('hex')); + `; + const result = spawnSync( + process.execPath, + ['--input-type=module', '--eval', script], + { + cwd: process.cwd(), + encoding: 'utf8', + env: { ...process.env, TZ: timezone }, + } + ); + expect(result.status, result.stderr).toBe(0); + return result.stdout; + } + ); + expect(new Set(hashes)).toHaveLength(1); + }); + + it('collects every installed runtime license text', async () => { + const bundle = await createThirdPartyLicenseBundle(process.cwd(), [ + '@add-ideas/toolbox-contract', + '@add-ideas/toolbox-shell-react', + 'react', + ]); + const text = bundle.toString('utf8'); + expect(text).toContain('@add-ideas/toolbox-contract@0.1.1'); + expect(text).toContain('@add-ideas/toolbox-shell-react@0.1.1'); + expect(text).toContain('react@'); + expect(createHash('sha256').update(bundle).digest('hex')).toMatch( + /^[a-f0-9]{64}$/ + ); + }); + + it('packages parser provenance and every required license text', async () => { + const moduleUrl = pathToFileURL( + path.join(process.cwd(), 'scripts', 'package-release.mjs') + ).href; + const { collectReleaseMetadataFiles } = (await import( + moduleUrl + )) as PackageReleaseModule; + const files = await collectReleaseMetadataFiles(process.cwd()); + + expect(Object.keys(files).sort()).toEqual( + expect.arrayContaining([ + 'LICENSE', + 'LICENSES/AGPL-3.0.txt', + 'LICENSES/MPL-2.0.txt', + 'LICENSES/THIRD-PARTY-LICENSES.txt', + 'LICENSES/lzxd-MIT.txt', + 'LICENSES/rust-cab-MIT.txt', + 'THIRD_PARTY_NOTICES.md', + 'docs/FORMAT_SUPPORT.md', + 'docs/PORTING.md', + 'docs/REFERENCE_IMPLEMENTATIONS.md', + 'docs/SECURITY.md', + 'tests/fixtures/README.md', + ]) + ); + expect( + createHash('sha256') + .update(files['LICENSES/rust-cab-MIT.txt']!) + .digest('hex') + ).toBe('ebaac853b53fec0ed475796f533ce676912a4df9ad9d8a621fa76d5060591e10'); + expect( + createHash('sha256').update(files['LICENSES/lzxd-MIT.txt']!).digest('hex') + ).toBe('c7a1dcbf839b1dfb72f24e1de12ab55f789db2c61d894ca9aa8b5f595490d9ca'); + expect(files).not.toHaveProperty( + 'tests/fixtures/joplin-test.onepkg.base64' + ); + expect(files).not.toHaveProperty('tests/fixtures/onenote_desktop.one.b64'); + }); + + it('rejects symbolic links in release input', async () => { + const temporaryDirectory = await mkdtemp( + path.join(os.tmpdir(), 'onenote-tools-release-') + ); + try { + const outsideFile = path.join(temporaryDirectory, 'outside.txt'); + const inputDirectory = path.join(temporaryDirectory, 'input'); + await writeFile(outsideFile, 'private'); + await mkdir(inputDirectory); + await symlink(outsideFile, path.join(inputDirectory, 'linked.txt')); + + await expect(collectDirectoryFiles(inputDirectory)).rejects.toThrow( + 'Unsupported filesystem entry in release input: linked.txt' + ); + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }); + } + }); +}); diff --git a/tools/oracle/README.md b/tools/oracle/README.md new file mode 100644 index 0000000..8be693a --- /dev/null +++ b/tools/oracle/README.md @@ -0,0 +1,5 @@ +# Development oracle + +The optional `onenote.rs` oracle is not wired in Phase 0. When added, it will +run only as an explicit development command against redistributable fixtures. +The production build and normal test suite will remain Rust-free. diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..74a5e82 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "noUncheckedIndexedAccess": true, + "types": ["node", "vitest/globals", "@testing-library/jest-dom"] + }, + "include": ["src", "tests"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..506d2e8 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "types": ["node"] + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..eefa92b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,14 @@ +/// +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + base: './', + plugins: [react()], + test: { + environment: 'jsdom', + setupFiles: './src/test/setup.ts', + css: true, + restoreMocks: true, + }, +});